fix: universal agent name replacement for non-Claude runtimes (#766) (#1195)

* fix: universal agent name replacement for non-Claude runtimes (#766)

Adds neutralizeAgentReferences() shared function that all non-Claude
runtime converters call to replace Claude-specific references:

- 'Claude' (standalone agent name) → 'the agent'
- 'CLAUDE.md' → runtime-specific file (AGENTS.md, GEMINI.md, COPILOT.md)
- Removes 'Do NOT load full AGENTS.md' (harmful for AGENTS.md runtimes)

Preserves: 'Claude Code' (product), 'Claude Opus/Sonnet/Haiku' (models),
'claude-' prefixes (packages, CSS classes).

Integrated into: OpenCode, Gemini, Copilot, Antigravity, and Codex
converters. Claude Code converter unchanged (references are correct).

Includes 7 new tests covering all replacement rules.

Closes #766

* fix: use copilot-instructions.md instead of COPILOT.md for Copilot runtime

Addresses review from Solvely-Colin: Copilot's actual instruction file
is copilot-instructions.md, not COPILOT.md. The neutralizer was mapping
CLAUDE.md -> COPILOT.md which would reference a non-existent file.

- install.js: pass 'copilot-instructions.md' to neutralizeAgentReferences
- runtime-converters.test.cjs: update test to validate correct filename
This commit is contained in:
Tom Boucher
2026-03-18 19:20:58 -04:00
committed by GitHub
parent 973c6b267d
commit 5fd384f336
2 changed files with 96 additions and 1 deletions

View File

@@ -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 <sub>text</sub> 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>(.*?)<\/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,

View File

@@ -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');
});
});