feat(cli+installer): codegraph explore/node CLI + instructions-file block — subagent & non-MCP reach (#704) (#819)

Task-tool subagents never see the MCP initialize instructions and hold
the MCP tools only as deferred names they rarely think to load — so
delegated work bypassed codegraph almost entirely (measured ~1 of 9
forced-delegation runs touched it; the rest did 30-50 grep/read calls).
Two additions close the gap:

- CLI: `codegraph explore` and `codegraph node` call the same ToolHandler
  as the MCP tools and print identical output — the graph for any agent
  with a shell (subagents, Gemini CLI, raw Codex, humans).
- Installer: each agent target (claude/codex/gemini/opencode) writes a
  short marker-fenced CodeGraph section into its instructions file —
  the one channel subagents DO receive — naming both surfaces. Upsert
  self-heals the stale pre-#529 long block; uninstall strips it; re-runs
  are byte-equal unchanged. (#529's duplication argument bounded the
  size: four lines, commands only.)

A/B (excalidraw, sonnet/high, forced Explore-agent delegation): without
the block, subagent codegraph usage ~1/9 runs; with it, 4/4 — subagents
ToolSearch-load the MCP tools and run explore 5-7x, best runs with ZERO
Read/grep (80-95s vs 150-197s baseline). The block's mechanism: the
parent relays the note into the task prompt, making the deferred tool
names salient.

Contract tests updated to the new expectations (write + self-heal
replace the #529 strip-only behavior); README install/guidance sections
refreshed (they also still described the pre-#817/#818 tool surface).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 21:50:41 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent c450fd95b7
commit 8170d181f2
10 changed files with 226 additions and 70 deletions
+41 -30
View File
@@ -192,20 +192,23 @@ describe('Installer targets — partial-state idempotency', () => {
fs.rmSync(tmpCwd, { recursive: true, force: true });
});
it('codex: install writes config.toml but never an AGENTS.md instructions file (#529)', () => {
it('codex: install writes config.toml AND the AGENTS.md codegraph block (#704)', () => {
const codex = getTarget('codex')!;
const first = codex.install('global', { autoAllow: false });
const agentsMd = path.join(tmpHome, '.codex', 'AGENTS.md');
// 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).
// The short instructions block IS written (subagents / non-MCP
// harnesses read AGENTS.md but never the MCP initialize instructions).
expect(fs.existsSync(agentsMd)).toBe(true);
const body = fs.readFileSync(agentsMd, 'utf-8');
expect(body).toContain('## CodeGraph');
expect(body).toContain('codegraph explore');
// Re-install is fully unchanged (byte-equal block → idempotent).
const second = codex.install('global', { autoAllow: false });
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)', () => {
it('codex: install replaces a legacy AGENTS.md codegraph block with the current one, keeping user content', () => {
const codex = getTarget('codex')!;
const dir = path.join(tmpHome, '.codex');
fs.mkdirSync(dir, { recursive: true });
@@ -217,10 +220,11 @@ describe('Installer targets — partial-state idempotency', () => {
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.
// Self-heal: the stale pre-#529 body is gone, the current block is in.
expect(body).not.toContain('Prefer `codegraph_search`');
expect(body).toContain('codegraph explore');
const mdEntry = result.files.find((f) => f.path.endsWith('AGENTS.md'));
expect(mdEntry?.action).toBe('removed');
expect(mdEntry?.action).toBe('updated');
});
it('opencode: prefers .jsonc when both .json and .jsonc exist', () => {
@@ -290,15 +294,16 @@ describe('Installer targets — partial-state idempotency', () => {
expect(fs.readFileSync(file, 'utf-8')).toBe(afterInstall);
});
it('opencode: install does NOT write an AGENTS.md instructions file (#529)', () => {
it('opencode: install writes the AGENTS.md codegraph block (#704)', () => {
const opencode = getTarget('opencode')!;
const result = opencode.install('global', { autoAllow: true });
const agentsMd = path.join(tmpHome, '.config', 'opencode', 'AGENTS.md');
expect(fs.existsSync(agentsMd)).toBe(false);
expect(result.files.some((f) => f.path.endsWith('AGENTS.md'))).toBe(false);
expect(fs.existsSync(agentsMd)).toBe(true);
expect(fs.readFileSync(agentsMd, 'utf-8')).toContain('codegraph explore');
expect(result.files.find((f) => f.path.endsWith('AGENTS.md'))?.action).toBe('created');
});
it('opencode: install strips a legacy AGENTS.md codegraph block, preserving user content (#529)', () => {
it('opencode: install replaces a legacy AGENTS.md codegraph block, preserving user content', () => {
const opencode = getTarget('opencode')!;
const dir = path.join(tmpHome, '.config', 'opencode');
fs.mkdirSync(dir, { recursive: true });
@@ -310,8 +315,9 @@ describe('Installer targets — partial-state idempotency', () => {
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(result.files.find((f) => f.path.endsWith('AGENTS.md'))?.action).toBe('removed');
expect(body).not.toContain('Prefer `codegraph_search`');
expect(body).toContain('codegraph explore');
expect(result.files.find((f) => f.path.endsWith('AGENTS.md'))?.action).toBe('updated');
});
it('opencode: uninstall strips a leftover codegraph block from AGENTS.md, keeping user content', () => {
@@ -329,24 +335,25 @@ describe('Installer targets — partial-state idempotency', () => {
expect(body).not.toContain('CODEGRAPH_START');
});
it('opencode: local install writes ./opencode.jsonc and never an ./AGENTS.md (#529)', () => {
it('opencode: local install writes ./opencode.jsonc and the ./AGENTS.md block (#704)', () => {
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(false);
expect(fs.existsSync(path.join(process.cwd(), 'AGENTS.md'))).toBe(false);
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
expect(fs.existsSync(path.join(process.cwd(), 'AGENTS.md'))).toBe(true);
});
it('gemini: install writes settings.json (mcpServers.codegraph) and no GEMINI.md (#529)', () => {
it('gemini: install writes settings.json (mcpServers.codegraph) and the GEMINI.md block (#704)', () => {
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(false);
expect(fs.existsSync(geminiMd)).toBe(false);
expect(result.files.some((f) => f.path === geminiMd)).toBe(true);
expect(fs.existsSync(geminiMd)).toBe(true);
expect(fs.readFileSync(geminiMd, 'utf-8')).toContain('codegraph explore');
const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
@@ -383,13 +390,13 @@ describe('Installer targets — partial-state idempotency', () => {
expect(after.mcpServers).toBeUndefined();
});
it('gemini: local install writes ./.gemini/settings.json and never a ./GEMINI.md (#529)', () => {
it('gemini: local install writes ./.gemini/settings.json and the project-root ./GEMINI.md block (#704)', () => {
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);
expect(paths.some((p) => p.endsWith('/GEMINI.md'))).toBe(false);
expect(fs.existsSync(path.join(process.cwd(), 'GEMINI.md'))).toBe(false);
expect(paths.some((p) => p.endsWith('/GEMINI.md'))).toBe(true);
expect(fs.existsSync(path.join(process.cwd(), 'GEMINI.md'))).toBe(true);
});
it('gemini: uninstall strips a leftover GEMINI.md codegraph block, keeping user content', () => {
@@ -880,15 +887,18 @@ describe('Installer targets — partial-state idempotency', () => {
expect(cfg.mcpServers.codegraph).toBeDefined();
});
it('claude: install does NOT create a CLAUDE.md instructions file (#529)', () => {
it('claude: install creates the CLAUDE.md codegraph block (#704)', () => {
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);
expect(fs.existsSync(claudeMd)).toBe(true);
const body = fs.readFileSync(claudeMd, 'utf-8');
expect(body).toContain('## CodeGraph');
expect(body).toContain('codegraph explore');
expect(result.files.find((f) => f.path.endsWith('CLAUDE.md'))?.action).toBe('created');
});
it('claude: install strips a legacy CLAUDE.md codegraph block, keeping user content (#529)', () => {
it('claude: install replaces a legacy CLAUDE.md codegraph block, keeping user content', () => {
const claude = getTarget('claude')!;
const claudeMd = path.join(tmpCwd, '.claude', 'CLAUDE.md');
fs.mkdirSync(path.dirname(claudeMd), { recursive: true });
@@ -899,8 +909,9 @@ describe('Installer targets — partial-state idempotency', () => {
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');
expect(body).not.toContain('Prefer `codegraph_search`');
expect(body).toContain('codegraph explore');
expect(result.files.find((f) => f.path.endsWith('CLAUDE.md'))?.action).toBe('updated');
});
it('claude: global install targets ~/.claude.json (user scope)', () => {