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:
co-authored by
Claude Opus 4.8
parent
c450fd95b7
commit
8170d181f2
@@ -896,6 +896,103 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph explore <query...>
|
||||
*
|
||||
* The CLI face of the MCP codegraph_explore tool — same handler, same
|
||||
* output (source of the relevant symbols grouped by file + the call path
|
||||
* among them). Exists so agents WITHOUT the MCP tools — Task-tool
|
||||
* subagents (which don't inherit MCP tools, #704) and non-MCP harnesses —
|
||||
* can reach the graph through a plain shell command.
|
||||
*/
|
||||
program
|
||||
.command('explore <query...>')
|
||||
.description('Explore an area: relevant symbols\' source + call paths in one shot (same output as the codegraph_explore MCP tool)')
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.option('--max-files <number>', 'Maximum number of files to include source from')
|
||||
.action(async (queryParts: string[], options: { path?: string; maxFiles?: string }) => {
|
||||
const projectPath = resolveProjectPath(options.path);
|
||||
|
||||
try {
|
||||
if (!isInitialized(projectPath)) {
|
||||
error(`CodeGraph not initialized in ${projectPath} — run 'codegraph init' first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { default: CodeGraph } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.open(projectPath);
|
||||
const { ToolHandler } = await import('../mcp/tools');
|
||||
const handler = new ToolHandler(cg);
|
||||
|
||||
const args: Record<string, unknown> = { query: queryParts.join(' ') };
|
||||
if (options.maxFiles) args.maxFiles = parseInt(options.maxFiles, 10);
|
||||
const result = await handler.execute('codegraph_explore', args);
|
||||
|
||||
console.log(result.content[0]?.text ?? '');
|
||||
cg.destroy();
|
||||
if (result.isError) process.exit(1);
|
||||
} catch (err) {
|
||||
error(`Explore failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph node <name>
|
||||
*
|
||||
* The CLI face of the MCP codegraph_node tool: one symbol's source +
|
||||
* caller/callee trail, or a whole file with line numbers + dependents
|
||||
* (Read-parity). Same subagent/non-MCP rationale as `explore`.
|
||||
*/
|
||||
program
|
||||
.command('node <name>')
|
||||
.description('One symbol\'s source + caller/callee trail, or read a file with line numbers + dependents (same output as the codegraph_node MCP tool)')
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.option('-f, --file <file>', 'Treat as file mode (or disambiguate a symbol to this file)')
|
||||
.option('--offset <number>', 'File mode: 1-based start line')
|
||||
.option('--limit <number>', 'File mode: maximum lines')
|
||||
.option('--symbols-only', 'File mode: just the symbol map + dependents')
|
||||
.action(async (name: string, options: { path?: string; file?: string; offset?: string; limit?: string; symbolsOnly?: boolean }) => {
|
||||
const projectPath = resolveProjectPath(options.path);
|
||||
|
||||
try {
|
||||
if (!isInitialized(projectPath)) {
|
||||
error(`CodeGraph not initialized in ${projectPath} — run 'codegraph init' first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { default: CodeGraph } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.open(projectPath);
|
||||
const { ToolHandler } = await import('../mcp/tools');
|
||||
const handler = new ToolHandler(cg);
|
||||
|
||||
// A name with a path separator is a file read; otherwise a symbol
|
||||
// (use --file for basename-only file reads or to pin an overload).
|
||||
const args: Record<string, unknown> = {};
|
||||
if (options.file) {
|
||||
args.file = options.file;
|
||||
if (name && name !== options.file) args.symbol = name;
|
||||
} else if (name.includes('/')) {
|
||||
args.file = name;
|
||||
} else {
|
||||
args.symbol = name;
|
||||
args.includeCode = true;
|
||||
}
|
||||
if (options.offset) args.offset = parseInt(options.offset, 10);
|
||||
if (options.limit) args.limit = parseInt(options.limit, 10);
|
||||
if (options.symbolsOnly) args.symbolsOnly = true;
|
||||
|
||||
const result = await handler.execute('codegraph_node', args);
|
||||
|
||||
console.log(result.content[0]?.text ?? '');
|
||||
cg.destroy();
|
||||
if (result.isError) process.exit(1);
|
||||
} catch (err) {
|
||||
error(`Node lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph files [path]
|
||||
*/
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
/**
|
||||
* Marker constants for the legacy agent-instructions block.
|
||||
* The marker-fenced agent-instructions block the installer writes into each
|
||||
* agent's instructions file (CLAUDE.md / AGENTS.md / GEMINI.md).
|
||||
*
|
||||
* Codegraph used to write a `## CodeGraph` usage guide into each
|
||||
* agent's instructions file (CLAUDE.md / AGENTS.md / GEMINI.md /
|
||||
* codegraph.mdc / Kiro steering doc). That duplicated the guidance the
|
||||
* MCP server already emits in its `initialize` response — every agent
|
||||
* read the same playbook twice each turn (issue #529). The installer no
|
||||
* longer writes an instructions file; the MCP server instructions in
|
||||
* `mcp/server-instructions.ts` are the single source of truth.
|
||||
* History: pre-#529 the installer wrote a full usage playbook here, which
|
||||
* duplicated the MCP `initialize` instructions for the main agent — so it
|
||||
* was removed and `mcp/server-instructions.ts` became the single source of
|
||||
* truth. A much smaller block returned for #704, because the MCP
|
||||
* instructions cannot reach two audiences that the instructions FILE does
|
||||
* reach:
|
||||
*
|
||||
* These markers are retained so install (self-heal on upgrade) and
|
||||
* uninstall can find and strip the block a previous install wrote.
|
||||
* - **Task-tool subagents** — they receive the project instructions file
|
||||
* in their context but NOT the MCP initialize instructions. They hold
|
||||
* the codegraph MCP tools only as deferred names and rarely think to
|
||||
* load them: measured on a forced-delegation flow question (excalidraw,
|
||||
* sonnet, high effort), subagents loaded + used codegraph in ~1 of 9
|
||||
* runs without this block, and consistently with it — including runs
|
||||
* with zero Read/grep fallback.
|
||||
* - **Non-MCP harnesses** — agents with no MCP client at all can still
|
||||
* run the `codegraph explore` / `codegraph node` CLI, which prints the
|
||||
* same output as the MCP tools.
|
||||
*
|
||||
* Keep this block SHORT. The main agent reads it every turn on top of the
|
||||
* server instructions — the #529 duplication-cost argument still bounds
|
||||
* its size. Command names and the two surfaces, nothing more.
|
||||
*/
|
||||
|
||||
/** Markers used by the marker-based section removal. */
|
||||
/** Markers used by the marker-based section write/removal. */
|
||||
export const CODEGRAPH_SECTION_START = '<!-- CODEGRAPH_START -->';
|
||||
export const CODEGRAPH_SECTION_END = '<!-- CODEGRAPH_END -->';
|
||||
|
||||
/** The full block, markers included, exactly as written to disk. */
|
||||
export const CODEGRAPH_INSTRUCTIONS_BLOCK = `${CODEGRAPH_SECTION_START}
|
||||
## CodeGraph
|
||||
|
||||
This repository is indexed by CodeGraph — a pre-built code knowledge graph. Reach for it BEFORE grep/find or reading files when you need to understand or locate code:
|
||||
|
||||
- **MCP tools** (when available): \`codegraph_explore\` answers most code questions in one call — the relevant symbols' verbatim source plus the call paths between them. \`codegraph_node\` returns one symbol's source + callers, or reads a whole file with line numbers. If the tools are listed but deferred, load them by name via tool search.
|
||||
- **Shell** (always works): \`codegraph explore "<symbol names or question>"\` and \`codegraph node <symbol-or-file>\` print the same output.
|
||||
${CODEGRAPH_SECTION_END}`;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
readJsonFile,
|
||||
removeMarkedSection,
|
||||
writeJsonFile,
|
||||
upsertInstructionsEntry,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
@@ -120,15 +121,12 @@ class ClaudeCodeTarget implements AgentTarget {
|
||||
const hookCleanup = cleanupLegacyHooks(loc);
|
||||
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
||||
|
||||
// 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);
|
||||
// 3. CLAUDE.md instructions — the short marker-fenced CodeGraph
|
||||
// block (#704). The MCP initialize instructions reach only the main
|
||||
// agent; CLAUDE.md is what Task-tool subagents (and non-MCP
|
||||
// harnesses) actually see, so the block carries the codegraph
|
||||
// pointers there. Upsert self-heals a stale pre-#529 long block.
|
||||
files.push(upsertInstructionsEntry(instructionsPath(loc)));
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
atomicWriteFileSync,
|
||||
getMcpServerConfig,
|
||||
removeMarkedSection,
|
||||
upsertInstructionsEntry,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
@@ -83,11 +84,10 @@ class CodexTarget implements AgentTarget {
|
||||
|
||||
files.push(writeMcpEntry());
|
||||
|
||||
// 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);
|
||||
// AGENTS.md gets the short marker-fenced CodeGraph block (#704):
|
||||
// subagents and non-MCP harnesses read AGENTS.md but never the MCP
|
||||
// initialize instructions. Upsert self-heals a stale pre-#529 block.
|
||||
files.push(upsertInstructionsEntry(instructionsPath()));
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
readJsonFile,
|
||||
removeMarkedSection,
|
||||
writeJsonFile,
|
||||
upsertInstructionsEntry,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
@@ -84,11 +85,10 @@ class GeminiTarget implements AgentTarget {
|
||||
const files: WriteResult['files'] = [];
|
||||
files.push(writeMcpEntry(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);
|
||||
// GEMINI.md gets the short marker-fenced CodeGraph block (#704):
|
||||
// subagents and non-MCP harnesses read GEMINI.md but never the MCP
|
||||
// initialize instructions. Upsert self-heals a stale pre-#529 block.
|
||||
files.push(upsertInstructionsEntry(instructionsPath(loc)));
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
atomicWriteFileSync,
|
||||
jsonDeepEqual,
|
||||
removeMarkedSection,
|
||||
upsertInstructionsEntry,
|
||||
} from './shared';
|
||||
import {
|
||||
CODEGRAPH_SECTION_END,
|
||||
@@ -127,11 +128,10 @@ class OpencodeTarget implements AgentTarget {
|
||||
const files: WriteResult['files'] = [];
|
||||
files.push(writeMcpEntry(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);
|
||||
// AGENTS.md gets the short marker-fenced CodeGraph block (#704):
|
||||
// subagents and non-MCP harnesses read AGENTS.md but never the MCP
|
||||
// initialize instructions. Upsert self-heals a stale pre-#529 block.
|
||||
files.push(upsertInstructionsEntry(instructionsPath(loc)));
|
||||
|
||||
return { files };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
CODEGRAPH_INSTRUCTIONS_BLOCK,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
} from '../instructions-template';
|
||||
|
||||
/**
|
||||
* The MCP-server config block codegraph injects. Same shape across
|
||||
@@ -167,6 +172,26 @@ export function replaceOrAppendMarkedSection(
|
||||
return 'appended';
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the CodeGraph instructions block into an agent instructions
|
||||
* file (CLAUDE.md / AGENTS.md / GEMINI.md). The one write shared by
|
||||
* every target: self-heals a stale pre-#529 long block (markers match →
|
||||
* replaced by the current short one), appends after existing user
|
||||
* content otherwise, and reports `unchanged` on byte-equal re-runs so
|
||||
* install stays idempotent. See `instructions-template.ts` for why this
|
||||
* block exists (#704: subagents + non-MCP harnesses never see the MCP
|
||||
* initialize instructions).
|
||||
*/
|
||||
export function upsertInstructionsEntry(file: string): { path: string; action: 'created' | 'updated' | 'unchanged' } {
|
||||
const action = replaceOrAppendMarkedSection(
|
||||
file,
|
||||
CODEGRAPH_INSTRUCTIONS_BLOCK,
|
||||
CODEGRAPH_SECTION_START,
|
||||
CODEGRAPH_SECTION_END,
|
||||
);
|
||||
return { path: file, action: action === 'appended' ? 'updated' : action };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of `replaceOrAppendMarkedSection`. Strips the marker
|
||||
* block from `filePath` if present. If the file becomes empty after
|
||||
|
||||
Reference in New Issue
Block a user