fix(installer): stop duplicating agent instructions; MCP server is the single source of truth (#529) (#538)

The installer wrote a `## CodeGraph` usage block into each agent's
instructions file (CLAUDE.md / AGENTS.md / GEMINI.md / .cursor/rules /
Kiro steering) that duplicated, almost verbatim, the guidance the MCP
server already emits in its `initialize` response — so agents that
surface MCP instructions (Claude Code) read the same playbook twice
every turn.

All 6 instruction-writing targets (claude, cursor, codex, opencode,
gemini, kiro) now stop writing the block. install self-heals by
stripping a block a previous version wrote (uninstall already did), so
the next `codegraph install`/`uninstall` cleans up existing installs;
upgrading the package alone does not (the leftover block is harmless).
server-instructions.ts is now the single source of truth — the two
steers unique to the old template ("trust codegraph, don't re-verify
with grep" and the not-initialized -> `init -i` hint) are ported there.

Removes the now-dead INSTRUCTIONS_TEMPLATE / CLAUDE_MD_TEMPLATE,
claude-md-template.ts, writeClaudeMd / hasClaudeMdSection, and the
Cursor-only wireProjectSurfaces bootstrap. The install log learned a
"Removed" verb. Tests rewritten to the new contract + self-heal
coverage (140/140 installer tests pass).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-28 15:13:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent cea78ceb1b
commit a9c9e76d8c
18 changed files with 282 additions and 593 deletions
+133 -65
View File
@@ -55,6 +55,18 @@ function setHome(dir: string): { restore: () => void } {
};
}
// A marker-delimited CodeGraph block exactly as a previous installer
// wrote it. Issue #529: the installer no longer writes an instructions
// file, but install (self-heal on upgrade) and uninstall both still
// strip a block a prior install left, so we plant this to exercise it.
const LEGACY_BLOCK = [
'<!-- CODEGRAPH_START -->',
'## CodeGraph',
'',
'Prefer `codegraph_search` / `codegraph_callers` over grep.',
'<!-- CODEGRAPH_END -->',
].join('\n');
describe('Installer targets — contract', () => {
let tmpHome: string;
let tmpCwd: string;
@@ -180,23 +192,35 @@ describe('Installer targets — partial-state idempotency', () => {
fs.rmSync(tmpCwd, { recursive: true, force: true });
});
it('codex: install after only config.toml exists — second pass is fully unchanged', () => {
it('codex: install writes config.toml but never an AGENTS.md instructions file (#529)', () => {
const codex = getTarget('codex')!;
// First install creates both files.
codex.install('global', { autoAllow: false });
// Delete the AGENTS.md to simulate partial state (user wiped one file).
const first = codex.install('global', { autoAllow: false });
const agentsMd = path.join(tmpHome, '.codex', 'AGENTS.md');
expect(fs.existsSync(agentsMd)).toBe(true);
fs.unlinkSync(agentsMd);
// Reinstall — TOML stays unchanged, AGENTS.md is recreated.
// No instructions file is created, and no file action references it.
expect(fs.existsSync(agentsMd)).toBe(false);
expect(first.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
expect(first.files.some((f) => f.path.endsWith('config.toml'))).toBe(true);
// Re-install is fully unchanged (config.toml only, nothing to strip).
const second = codex.install('global', { autoAllow: false });
const tomlEntry = second.files.find((f) => f.path.endsWith('config.toml'))!;
const mdEntry = second.files.find((f) => f.path.endsWith('AGENTS.md'))!;
expect(tomlEntry.action).toBe('unchanged');
expect(mdEntry.action).toBe('created');
// Third install — both unchanged (full idempotency restored).
const third = codex.install('global', { autoAllow: false });
for (const f of third.files) expect(f.action).toBe('unchanged');
for (const f of second.files) expect(f.action).toBe('unchanged');
});
it('codex: install strips a legacy AGENTS.md codegraph block, keeping user content (#529)', () => {
const codex = getTarget('codex')!;
const dir = path.join(tmpHome, '.codex');
fs.mkdirSync(dir, { recursive: true });
const agentsMd = path.join(dir, 'AGENTS.md');
fs.writeFileSync(agentsMd, `# My codex notes\n\nBe terse.\n\n${LEGACY_BLOCK}\n`);
const result = codex.install('global', { autoAllow: false });
const body = fs.readFileSync(agentsMd, 'utf-8');
expect(body).toContain('# My codex notes');
expect(body).toContain('Be terse.');
expect(body).not.toContain('CODEGRAPH_START');
// The strip is reported as a 'removed' action on AGENTS.md.
const mdEntry = result.files.find((f) => f.path.endsWith('AGENTS.md'));
expect(mdEntry?.action).toBe('removed');
});
it('opencode: prefers .jsonc when both .json and .jsonc exist', () => {
@@ -266,72 +290,66 @@ describe('Installer targets — partial-state idempotency', () => {
expect(fs.readFileSync(file, 'utf-8')).toBe(afterInstall);
});
it('opencode: install writes AGENTS.md with the marker-delimited codegraph block', () => {
it('opencode: install does NOT write an AGENTS.md instructions file (#529)', () => {
const opencode = getTarget('opencode')!;
opencode.install('global', { autoAllow: true });
const result = opencode.install('global', { autoAllow: true });
const agentsMd = path.join(tmpHome, '.config', 'opencode', 'AGENTS.md');
expect(fs.existsSync(agentsMd)).toBe(true);
const body = fs.readFileSync(agentsMd, 'utf-8');
expect(body).toContain('<!-- CODEGRAPH_START -->');
expect(body).toContain('<!-- CODEGRAPH_END -->');
expect(body).toContain('codegraph_callers');
expect(fs.existsSync(agentsMd)).toBe(false);
expect(result.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
});
it('opencode: AGENTS.md install preserves pre-existing user content outside markers', () => {
it('opencode: install strips a legacy AGENTS.md codegraph block, preserving user content (#529)', () => {
const opencode = getTarget('opencode')!;
const dir = path.join(tmpHome, '.config', 'opencode');
fs.mkdirSync(dir, { recursive: true });
const agentsMd = path.join(dir, 'AGENTS.md');
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
fs.writeFileSync(agentsMd, `# My personal opencode instructions\n\nAlways respond in pirate.\n\n${LEGACY_BLOCK}\n`);
const result = opencode.install('global', { autoAllow: true });
opencode.install('global', { autoAllow: true });
const body = fs.readFileSync(agentsMd, 'utf-8');
expect(body).toContain('# My personal opencode instructions');
expect(body).toContain('Always respond in pirate.');
expect(body).toContain('<!-- CODEGRAPH_START -->');
expect(body).not.toContain('CODEGRAPH_START');
expect(result.files.find((f) => f.path.endsWith('AGENTS.md'))?.action).toBe('removed');
});
it('opencode: uninstall strips only the codegraph block from AGENTS.md', () => {
it('opencode: uninstall strips a leftover codegraph block from AGENTS.md, keeping user content', () => {
const opencode = getTarget('opencode')!;
const dir = path.join(tmpHome, '.config', 'opencode');
fs.mkdirSync(dir, { recursive: true });
const agentsMd = path.join(dir, 'AGENTS.md');
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
fs.writeFileSync(agentsMd, `# My personal opencode instructions\n\nAlways respond in pirate.\n\n${LEGACY_BLOCK}\n`);
opencode.install('global', { autoAllow: true });
opencode.uninstall('global');
const body = fs.readFileSync(agentsMd, 'utf-8');
expect(body).toContain('# My personal opencode instructions');
expect(body).toContain('Always respond in pirate.');
expect(body).not.toContain('CODEGRAPH_START');
expect(body).not.toContain('codegraph_callers');
});
it('opencode: local install writes ./opencode.jsonc and ./AGENTS.md in cwd', () => {
it('opencode: local install writes ./opencode.jsonc and never an ./AGENTS.md (#529)', () => {
const opencode = getTarget('opencode')!;
const result = opencode.install('local', { autoAllow: true });
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
// macOS realpath shenanigans (/var vs /private/var) — suffix match.
expect(paths.some((p) => p.endsWith('/opencode.jsonc'))).toBe(true);
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(false);
expect(fs.existsSync(path.join(process.cwd(), 'AGENTS.md'))).toBe(false);
});
it('gemini: install writes settings.json (mcpServers.codegraph) and GEMINI.md with marker block', () => {
it('gemini: install writes settings.json (mcpServers.codegraph) and no GEMINI.md (#529)', () => {
const gemini = getTarget('gemini')!;
const result = gemini.install('global', { autoAllow: true });
const settings = path.join(tmpHome, '.gemini', 'settings.json');
const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
expect(result.files.some((f) => f.path === settings)).toBe(true);
expect(result.files.some((f) => f.path === geminiMd)).toBe(true);
expect(result.files.some((f) => f.path === geminiMd)).toBe(false);
expect(fs.existsSync(geminiMd)).toBe(false);
const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
const md = fs.readFileSync(geminiMd, 'utf-8');
expect(md).toContain('<!-- CODEGRAPH_START -->');
expect(md).toContain('<!-- CODEGRAPH_END -->');
expect(md).toContain('codegraph_callers');
});
it('gemini: install preserves pre-existing settings (security.auth survives)', () => {
@@ -365,45 +383,51 @@ describe('Installer targets — partial-state idempotency', () => {
expect(after.mcpServers).toBeUndefined();
});
it('gemini: local install writes ./.gemini/settings.json and ./GEMINI.md (project root)', () => {
it('gemini: local install writes ./.gemini/settings.json and never a ./GEMINI.md (#529)', () => {
const gemini = getTarget('gemini')!;
const result = gemini.install('local', { autoAllow: true });
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.some((p) => p.endsWith('/.gemini/settings.json'))).toBe(true);
// Local GEMINI.md sits at the project root, NOT under .gemini/.
expect(paths.some((p) => p.endsWith('/GEMINI.md') && !p.endsWith('/.gemini/GEMINI.md'))).toBe(true);
expect(paths.some((p) => p.endsWith('/GEMINI.md'))).toBe(false);
expect(fs.existsSync(path.join(process.cwd(), 'GEMINI.md'))).toBe(false);
});
it('gemini: GEMINI.md uninstall preserves user content outside the codegraph markers', () => {
it('gemini: uninstall strips a leftover GEMINI.md codegraph block, keeping user content', () => {
const gemini = getTarget('gemini')!;
const geminiMd = path.join(tmpHome, '.gemini', 'GEMINI.md');
fs.mkdirSync(path.dirname(geminiMd), { recursive: true });
fs.writeFileSync(geminiMd, '# My personal Gemini context\n\nAlways respond concisely.\n');
fs.writeFileSync(geminiMd, `# My personal Gemini context\n\nAlways respond concisely.\n\n${LEGACY_BLOCK}\n`);
gemini.install('global', { autoAllow: true });
gemini.uninstall('global');
const body = fs.readFileSync(geminiMd, 'utf-8');
expect(body).toContain('# My personal Gemini context');
expect(body).toContain('Always respond concisely.');
expect(body).not.toContain('CODEGRAPH_START');
expect(body).not.toContain('codegraph_callers');
});
it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and steering/codegraph.md', () => {
it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and no steering doc (#529)', () => {
const kiro = getTarget('kiro')!;
const result = kiro.install('global', { autoAllow: true });
const mcp = path.join(tmpHome, '.kiro', 'settings', 'mcp.json');
const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
expect(result.files.some((f) => f.path === mcp)).toBe(true);
expect(result.files.some((f) => f.path === steering)).toBe(true);
expect(result.files.some((f) => f.path === steering)).toBe(false);
expect(fs.existsSync(steering)).toBe(false);
const cfg = JSON.parse(fs.readFileSync(mcp, 'utf-8'));
expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
});
const md = fs.readFileSync(steering, 'utf-8');
expect(md).toContain('codegraph_callers');
expect(md).toContain('CodeGraph MCP server');
it('kiro: install deletes a leftover steering codegraph.md (self-heal) (#529)', () => {
const kiro = getTarget('kiro')!;
const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
fs.mkdirSync(path.dirname(steering), { recursive: true });
fs.writeFileSync(steering, `${LEGACY_BLOCK}\n`);
const result = kiro.install('global', { autoAllow: true });
expect(fs.existsSync(steering)).toBe(false);
expect(result.files.find((f) => f.path === steering)?.action).toBe('removed');
});
it('kiro: install preserves a pre-existing sibling MCP server in mcp.json', () => {
@@ -437,35 +461,37 @@ describe('Installer targets — partial-state idempotency', () => {
expect(after.mcpServers.codegraph).toBeUndefined();
});
it('kiro: uninstall removes the steering codegraph.md file outright', () => {
it('kiro: uninstall removes a leftover steering codegraph.md file outright', () => {
const kiro = getTarget('kiro')!;
kiro.install('global', { autoAllow: true });
const steering = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
expect(fs.existsSync(steering)).toBe(true);
fs.mkdirSync(path.dirname(steering), { recursive: true });
fs.writeFileSync(steering, `${LEGACY_BLOCK}\n`);
kiro.uninstall('global');
expect(fs.existsSync(steering)).toBe(false);
});
it('kiro: uninstall leaves a sibling steering file (product.md) untouched', () => {
it('kiro: uninstall removes our steering doc but leaves a sibling (product.md) untouched', () => {
const kiro = getTarget('kiro')!;
const sibling = path.join(tmpHome, '.kiro', 'steering', 'product.md');
const ours = path.join(tmpHome, '.kiro', 'steering', 'codegraph.md');
fs.mkdirSync(path.dirname(sibling), { recursive: true });
fs.writeFileSync(sibling, '# Product\n\nMy team practices.\n');
fs.writeFileSync(ours, `${LEGACY_BLOCK}\n`);
kiro.install('global', { autoAllow: true });
kiro.uninstall('global');
expect(fs.existsSync(ours)).toBe(false);
expect(fs.existsSync(sibling)).toBe(true);
expect(fs.readFileSync(sibling, 'utf-8')).toContain('My team practices.');
});
it('kiro: local install writes ./.kiro/settings/mcp.json and ./.kiro/steering/codegraph.md', () => {
it('kiro: local install writes ./.kiro/settings/mcp.json and no steering doc (#529)', () => {
const kiro = getTarget('kiro')!;
const result = kiro.install('local', { autoAllow: true });
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.some((p) => p.endsWith('/.kiro/settings/mcp.json'))).toBe(true);
expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(true);
expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(false);
});
it('antigravity: install writes to LEGACY ~/.gemini/antigravity/mcp_config.json when no migration marker', () => {
@@ -854,6 +880,29 @@ describe('Installer targets — partial-state idempotency', () => {
expect(cfg.mcpServers.codegraph).toBeDefined();
});
it('claude: install does NOT create a CLAUDE.md instructions file (#529)', () => {
const claude = getTarget('claude')!;
const result = claude.install('local', { autoAllow: false });
const claudeMd = path.join(tmpCwd, '.claude', 'CLAUDE.md');
expect(fs.existsSync(claudeMd)).toBe(false);
expect(result.files.some((f) => f.path.endsWith('CLAUDE.md'))).toBe(false);
});
it('claude: install strips a legacy CLAUDE.md codegraph block, keeping user content (#529)', () => {
const claude = getTarget('claude')!;
const claudeMd = path.join(tmpCwd, '.claude', 'CLAUDE.md');
fs.mkdirSync(path.dirname(claudeMd), { recursive: true });
fs.writeFileSync(claudeMd, `# My project rules\n\nUse tabs.\n\n${LEGACY_BLOCK}\n`);
const result = claude.install('local', { autoAllow: false });
const body = fs.readFileSync(claudeMd, 'utf-8');
expect(body).toContain('# My project rules');
expect(body).toContain('Use tabs.');
expect(body).not.toContain('CODEGRAPH_START');
expect(result.files.find((f) => f.path.endsWith('CLAUDE.md'))?.action).toBe('removed');
});
it('claude: global install targets ~/.claude.json (user scope)', () => {
const claude = getTarget('claude')!;
claude.install('global', { autoAllow: false });
@@ -1282,22 +1331,41 @@ describe('Installer — Cursor rules file cleanup on uninstall', () => {
const rulesFile = () => path.join(process.cwd(), '.cursor', 'rules', 'codegraph.mdc');
it('deletes the dedicated codegraph.mdc entirely (no orphaned frontmatter left behind)', () => {
cursor.install('local', { autoAllow: true });
// The frontmatter a previous install wrote ahead of the marked block.
// `removeRulesEntry` recognizes it to decide whether the leftover .mdc
// is ours-to-delete or carries user content worth keeping.
const MDC_FRONTMATTER = [
'---',
'description: CodeGraph MCP usage guide — when to use which tool',
'alwaysApply: true',
'---',
'',
].join('\n');
function plantLegacyRulesFile(extra = ''): void {
fs.mkdirSync(path.dirname(rulesFile()), { recursive: true });
fs.writeFileSync(rulesFile(), MDC_FRONTMATTER + LEGACY_BLOCK + '\n' + extra);
}
it('uninstall deletes a leftover codegraph.mdc entirely (no orphaned frontmatter left behind)', () => {
plantLegacyRulesFile();
expect(fs.existsSync(rulesFile())).toBe(true);
cursor.uninstall('local');
// The whole file — frontmatter included — is gone, not just the block.
expect(fs.existsSync(rulesFile())).toBe(false);
expect(cursor.detect('local').alreadyConfigured).toBe(false);
});
it('preserves user content added outside the codegraph markers (strips only our block)', () => {
cursor.install('local', { autoAllow: true });
const withUserContent =
fs.readFileSync(rulesFile(), 'utf-8') + '\n## My own rule\nkeep me\n';
fs.writeFileSync(rulesFile(), withUserContent);
it('install self-heals a leftover codegraph.mdc (#529)', () => {
plantLegacyRulesFile();
const result = cursor.install('local', { autoAllow: true });
expect(fs.existsSync(rulesFile())).toBe(false);
expect(result.files.some((f) => f.path.endsWith('codegraph.mdc') && f.action === 'removed')).toBe(true);
});
it('uninstall preserves user content added outside the codegraph markers (strips only our block)', () => {
plantLegacyRulesFile('## My own rule\nkeep me\n');
cursor.uninstall('local');