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
+23 -50
View File
@@ -28,19 +28,16 @@ import {
WriteResult,
} from './types';
import {
atomicWriteFileSync,
getCodeGraphPermissions,
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
removeMarkedSection,
replaceOrAppendMarkedSection,
writeJsonFile,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
INSTRUCTIONS_TEMPLATE,
} from '../instructions-template';
function configDir(loc: Location): string {
@@ -123,8 +120,15 @@ class ClaudeCodeTarget implements AgentTarget {
const hookCleanup = cleanupLegacyHooks(loc);
if (hookCleanup.action === 'removed') files.push(hookCleanup);
// 3. CLAUDE.md instructions
files.push(writeInstructionsEntry(loc));
// 3. CLAUDE.md instructions — no longer written. The codegraph
// usage guidance now ships solely in the MCP server's `initialize`
// response (see `mcp/server-instructions.ts`), which Claude Code
// surfaces in the system prompt automatically. Writing it into
// CLAUDE.md as well meant the agent read the same playbook twice
// every turn (issue #529). Strip any block a previous install left
// behind so an upgrade self-heals — same idiom as the hook cleanup.
const instrCleanup = removeInstructionsEntry(loc);
if (instrCleanup.action === 'removed') files.push(instrCleanup);
return { files };
}
@@ -185,10 +189,8 @@ class ClaudeCodeTarget implements AgentTarget {
const hookCleanup = cleanupLegacyHooks(loc);
if (hookCleanup.action === 'removed') files.push(hookCleanup);
// 3. Instructions
const instr = instructionsPath(loc);
const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
files.push({ path: instr, action });
// 3. Instructions — strip the legacy CodeGraph block if present.
files.push(removeInstructionsEntry(loc));
return { files };
}
@@ -359,48 +361,19 @@ export function writePermissionsEntry(loc: Location): WriteResult['files'][numbe
return { path: file, action: created ? 'created' : 'updated' };
}
export function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
/**
* Strip the marker-delimited CodeGraph block from CLAUDE.md if a prior
* install wrote one. Codegraph no longer maintains an instructions file
* (issue #529) — the MCP server's `initialize` instructions are the
* single source of truth — so both install (self-heal on upgrade) and
* uninstall call this. `removeMarkedSection` returns `not-found`/`kept`
* when there's nothing to strip; the install caller drops those from
* the report so a fresh install stays quiet.
*/
export function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
const file = instructionsPath(loc);
// Ensure config dir exists (for global ~/.claude/).
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
// Honor the legacy "unmarked ## CodeGraph" rewrite path that the
// original installer supported (some users hand-pasted a section
// before markers existed). Detect first and migrate inline.
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf-8');
if (!content.includes(CODEGRAPH_SECTION_START)) {
const headerMatch = content.match(/\n## CodeGraph\n/);
if (headerMatch && headerMatch.index !== undefined) {
const sectionStart = headerMatch.index;
const after = content.substring(sectionStart + 1);
const nextHeader = after.match(/\n## (?!#)/);
const sectionEnd = nextHeader && nextHeader.index !== undefined
? sectionStart + 1 + nextHeader.index
: content.length;
const merged =
content.substring(0, sectionStart) +
'\n' + INSTRUCTIONS_TEMPLATE +
content.substring(sectionEnd);
atomicWriteFileSync(file, merged);
return { path: file, action: 'updated' };
}
}
}
const action = replaceOrAppendMarkedSection(
file,
INSTRUCTIONS_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
);
// Map the four-state action to WriteResult's action vocabulary.
const mapped: 'created' | 'updated' | 'unchanged' =
action === 'created' ? 'created'
: action === 'unchanged' ? 'unchanged'
: 'updated';
return { path: file, action: mapped };
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
return { path: file, action };
}
export const claudeTarget: AgentTarget = new ClaudeCodeTarget();
+15 -21
View File
@@ -28,12 +28,10 @@ import {
atomicWriteFileSync,
getMcpServerConfig,
removeMarkedSection,
replaceOrAppendMarkedSection,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
INSTRUCTIONS_TEMPLATE,
} from '../instructions-template';
import { buildTomlTable, removeTomlTable, upsertTomlTable } from './toml';
@@ -84,7 +82,12 @@ class CodexTarget implements AgentTarget {
const files: WriteResult['files'] = [];
files.push(writeMcpEntry());
files.push(writeInstructionsEntry());
// AGENTS.md is no longer written — the codegraph usage guidance
// ships in the MCP server's `initialize` response (issue #529).
// Strip a block a previous install left so an upgrade self-heals.
const instrCleanup = removeInstructionsEntry();
if (instrCleanup.action === 'removed') files.push(instrCleanup);
return { files };
}
@@ -111,9 +114,7 @@ class CodexTarget implements AgentTarget {
files.push({ path: tomlPath, action: 'not-found' });
}
const instr = instructionsPath();
const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
files.push({ path: instr, action: instrAction });
files.push(removeInstructionsEntry());
return { files };
}
@@ -160,22 +161,15 @@ function writeMcpEntry(): WriteResult['files'][number] {
return { path: file, action: created ? 'created' : 'updated' };
}
function writeInstructionsEntry(): WriteResult['files'][number] {
/**
* Strip the marker-delimited CodeGraph block from `~/.codex/AGENTS.md`
* if a prior install wrote one. Used by both install (self-heal on
* upgrade) and uninstall — see issue #529.
*/
function removeInstructionsEntry(): WriteResult['files'][number] {
const file = instructionsPath();
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const action = replaceOrAppendMarkedSection(
file,
INSTRUCTIONS_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
);
const mapped: 'created' | 'updated' | 'unchanged' =
action === 'created' ? 'created'
: action === 'unchanged' ? 'unchanged'
: 'updated';
return { path: file, action: mapped };
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
return { path: file, action };
}
export const codexTarget: AgentTarget = new CodexTarget();
+8 -51
View File
@@ -46,13 +46,11 @@ import {
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
replaceOrAppendMarkedSection,
writeJsonFile,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
INSTRUCTIONS_TEMPLATE,
} from '../instructions-template';
function mcpJsonPath(loc: Location): string {
@@ -112,8 +110,13 @@ class CursorTarget implements AgentTarget {
files.push(writeMcpEntry(loc));
// We no longer write `.cursor/rules/codegraph.mdc` — the codegraph
// usage guidance ships in the MCP server's `initialize` response,
// the single source of truth (issue #529). Strip a rules file a
// previous install created so an upgrade self-heals.
if (loc === 'local') {
files.push(writeRulesEntry());
const rulesCleanup = removeRulesEntry();
if (rulesCleanup.action === 'removed') files.push(rulesCleanup);
}
return {
@@ -156,16 +159,6 @@ class CursorTarget implements AgentTarget {
? [mcpJsonPath(loc), rulesPath()]
: [mcpJsonPath(loc)];
}
/**
* Write the project-local `.cursor/rules/codegraph.mdc` file. Used
* by `codegraph init` to bootstrap projects that have only the
* global `~/.cursor/mcp.json` — without the rules file, the Cursor
* agent has no signal to prefer codegraph over native grep.
*/
wireProjectSurfaces(): WriteResult {
return { files: [writeRulesEntry()] };
}
}
/**
@@ -197,45 +190,9 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
return { path: file, action };
}
function writeRulesEntry(): WriteResult['files'][number] {
const file = rulesPath();
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
// Body is frontmatter + the shared instructions block. The
// marker-based replacement targets only the marker block, so the
// frontmatter is preserved across re-runs.
const body = MDC_FRONTMATTER + INSTRUCTIONS_TEMPLATE;
if (!fs.existsSync(file)) {
atomicWriteFileSync(file, body + '\n');
return { path: file, action: 'created' };
}
// For .mdc files we own outright, do byte-equality first.
const existing = fs.readFileSync(file, 'utf-8');
const wantWithNL = body + '\n';
if (existing === wantWithNL) {
return { path: file, action: 'unchanged' };
}
// Otherwise, marker-based section swap (preserves any user-added
// content outside the markers).
const action = replaceOrAppendMarkedSection(
file,
INSTRUCTIONS_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
);
const mapped: 'created' | 'updated' | 'unchanged' =
action === 'created' ? 'created'
: action === 'unchanged' ? 'unchanged'
: 'updated';
return { path: file, action: mapped };
}
/**
* Remove the Cursor rules file on uninstall.
* Remove the Cursor rules file on uninstall (and as a self-heal on
* install — see issue #529).
*
* Unlike the shared CLAUDE.md / AGENTS.md files (where codegraph owns
* only a marker-delimited section), `.cursor/rules/codegraph.mdc` is a
+16 -21
View File
@@ -37,13 +37,11 @@ import {
jsonDeepEqual,
readJsonFile,
removeMarkedSection,
replaceOrAppendMarkedSection,
writeJsonFile,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
INSTRUCTIONS_TEMPLATE,
} from '../instructions-template';
function configDir(loc: Location): string {
@@ -85,7 +83,13 @@ class GeminiTarget implements AgentTarget {
install(loc: Location, _opts: InstallOptions): WriteResult {
const files: WriteResult['files'] = [];
files.push(writeMcpEntry(loc));
files.push(writeInstructionsEntry(loc));
// GEMINI.md is no longer written — the codegraph usage guidance
// ships in the MCP server's `initialize` response (issue #529).
// Strip a block a previous install left so an upgrade self-heals.
const instrCleanup = removeInstructionsEntry(loc);
if (instrCleanup.action === 'removed') files.push(instrCleanup);
return { files };
}
@@ -108,9 +112,7 @@ class GeminiTarget implements AgentTarget {
files.push({ path: file, action: 'not-found' });
}
const instr = instructionsPath(loc);
const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
files.push({ path: instr, action });
files.push(removeInstructionsEntry(loc));
return { files };
}
@@ -146,22 +148,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
return { path: file, action };
}
function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
/**
* Strip the marker-delimited CodeGraph block from GEMINI.md if a prior
* install wrote one. Used by both install (self-heal on upgrade) and
* uninstall — see issue #529.
*/
function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
const file = instructionsPath(loc);
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const action = replaceOrAppendMarkedSection(
file,
INSTRUCTIONS_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
);
const mapped: 'created' | 'updated' | 'unchanged' =
action === 'created' ? 'created'
: action === 'unchanged' ? 'unchanged'
: 'updated';
return { path: file, action: mapped };
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
return { path: file, action };
}
export const geminiTarget: AgentTarget = new GeminiTarget();
+10 -30
View File
@@ -34,13 +34,11 @@ import {
WriteResult,
} from './types';
import {
atomicWriteFileSync,
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
writeJsonFile,
} from './shared';
import { INSTRUCTIONS_TEMPLATE } from '../instructions-template';
function configDir(loc: Location): string {
return loc === 'global'
@@ -76,7 +74,14 @@ class KiroTarget implements AgentTarget {
install(loc: Location, _opts: InstallOptions): WriteResult {
const files: WriteResult['files'] = [];
files.push(writeMcpEntry(loc));
files.push(writeSteeringEntry(loc));
// The steering doc is no longer written — the codegraph usage
// guidance ships in the MCP server's `initialize` response (issue
// #529). Delete a `codegraph.md` a previous install created so an
// upgrade self-heals.
const steeringCleanup = removeSteeringEntry(loc);
if (steeringCleanup.action === 'removed') files.push(steeringCleanup);
return {
files,
// The IDE-only enable-MCP step is load-bearing: Kiro IDE ships
@@ -143,37 +148,12 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
return { path: file, action };
}
/**
* Write the dedicated steering file. Unlike CLAUDE.md / GEMINI.md
* (shared files where codegraph owns a marker-delimited section),
* Kiro's steering dir loads every `*.md` as a discrete document — so
* `codegraph.md` is ours outright. Byte-equality short-circuits
* idempotent re-runs; mismatched content gets a clean rewrite.
*/
function writeSteeringEntry(loc: Location): WriteResult['files'][number] {
const file = steeringPath(loc);
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const body = INSTRUCTIONS_TEMPLATE + '\n';
if (!fs.existsSync(file)) {
atomicWriteFileSync(file, body);
return { path: file, action: 'created' };
}
const existing = fs.readFileSync(file, 'utf-8');
if (existing === body) {
return { path: file, action: 'unchanged' };
}
atomicWriteFileSync(file, body);
return { path: file, action: 'updated' };
}
/**
* Delete the steering file we own. If a user has hand-edited the file
* out of recognition we still remove it — codegraph.md is a name we
* claim, and a partial install leaving the file behind is worse than
* a clean delete.
* a clean delete. Used by both install (self-heal on upgrade — see
* issue #529) and uninstall.
*/
function removeSteeringEntry(loc: Location): WriteResult['files'][number] {
const file = steeringPath(loc);
+16 -21
View File
@@ -41,12 +41,10 @@ import {
atomicWriteFileSync,
jsonDeepEqual,
removeMarkedSection,
replaceOrAppendMarkedSection,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
INSTRUCTIONS_TEMPLATE,
} from '../instructions-template';
function globalConfigDir(): string {
@@ -128,7 +126,13 @@ class OpencodeTarget implements AgentTarget {
install(loc: Location, _opts: InstallOptions): WriteResult {
const files: WriteResult['files'] = [];
files.push(writeMcpEntry(loc));
files.push(writeInstructionsEntry(loc));
// AGENTS.md is no longer written — the codegraph usage guidance
// ships in the MCP server's `initialize` response (issue #529).
// Strip a block a previous install left so an upgrade self-heals.
const instrCleanup = removeInstructionsEntry(loc);
if (instrCleanup.action === 'removed') files.push(instrCleanup);
return { files };
}
@@ -163,9 +167,7 @@ class OpencodeTarget implements AgentTarget {
}
}
const instr = instructionsPath(loc);
const instrAction = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
files.push({ path: instr, action: instrAction });
files.push(removeInstructionsEntry(loc));
return { files };
}
@@ -223,22 +225,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
return { path: file, action: existed ? 'updated' : 'created' };
}
function writeInstructionsEntry(loc: Location): WriteResult['files'][number] {
/**
* Strip the marker-delimited CodeGraph block from AGENTS.md if a prior
* install wrote one. Used by both install (self-heal on upgrade) and
* uninstall — see issue #529.
*/
function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
const file = instructionsPath(loc);
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const action = replaceOrAppendMarkedSection(
file,
INSTRUCTIONS_TEMPLATE,
CODEGRAPH_SECTION_START,
CODEGRAPH_SECTION_END,
);
const mapped: 'created' | 'updated' | 'unchanged' =
action === 'created' ? 'created'
: action === 'unchanged' ? 'unchanged'
: 'updated';
return { path: file, action: mapped };
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
return { path: file, action };
}
export const opencodeTarget: AgentTarget = new OpencodeTarget();
-15
View File
@@ -103,19 +103,4 @@ export interface AgentTarget {
printConfig(loc: Location): string;
/** Filesystem paths this target would write to at this location. */
describePaths(loc: Location): string[];
/**
* Optional. Write any project-local surfaces this target needs in
* order to work fully when its MCP config is configured globally.
* Called by `codegraph init` to bootstrap new projects without
* forcing the user to re-run `codegraph install` per project.
*
* Most targets need nothing here — their global config is complete.
* Cursor is the notable exception: its rules system
* (`.cursor/rules/*.mdc`) is project-scoped only, and is what makes
* Cursor's agent prefer codegraph over its built-in grep.
*
* Must be idempotent. Targets that have nothing project-local omit
* the method entirely.
*/
wireProjectSurfaces?(): WriteResult;
}