Bladeren bron

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>
Colby Mchenry 1 week geleden
bovenliggende
commit
8170d181f2

+ 1 - 0
CHANGELOG.md

@@ -16,6 +16,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### New Features
 
+- **Subagents and non-MCP agents can now reach CodeGraph.** Two new CLI commands — `codegraph explore "<symbols or question>"` and `codegraph node <symbol-or-file>` — print exactly what the matching MCP tools return (relevant symbols' source + call paths; one symbol's source + callers; file reads with line numbers), so any agent with a shell can use the graph. And `codegraph install` now writes a small marker-fenced CodeGraph section into each agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) pointing at both surfaces — that file is what Task-tool subagents actually see, where the MCP server's own guidance only reaches the main agent. Measured on a delegated code-exploration task: subagents went from almost never using CodeGraph (~1 in 9 runs) to using it in every run, including runs with zero grep/file-reading fallback. The section is small, survives your own content, upgrades cleanly from the old long block, and `codegraph uninstall` removes it. Thanks @liuyao37511. (#704)
 - **The MCP tool list is now a focused default of four** — `codegraph_explore`, `codegraph_node`, `codegraph_search`, and `codegraph_callers`. The other four (`codegraph_callees`, `codegraph_impact`, `codegraph_files`, `codegraph_status`) remain fully functional — the CLI and library API are unchanged, and `CODEGRAPH_MCP_TOOLS` re-enables any of them — but they're no longer listed to agents by default: measured agent behavior shows they're never or rarely picked, and the information they carry already arrives inline on the tools agents do use (explore's blast-radius section, node's dependents note, a symbol's own body as its callee list). A leaner list saves context tokens every session and steers agents to the right tool by presence alone.
 - **CodeGraph now goes quiet instead of failing loudly in unindexed projects.** When an AI agent's session starts in a workspace that has no CodeGraph index, the MCP server now announces itself as inactive with a short note and lists no tools at all — instead of presenting the full toolset and erroring on every call, which taught agents to distrust CodeGraph even where it works. Querying another project that isn't indexed likewise returns clear guidance (use your regular tools for that codebase; the user can run `codegraph init` there to enable CodeGraph) instead of an error, and genuine internal errors now tell the agent to retry once rather than give up on CodeGraph entirely. Indexing stays your decision — agents are told not to run it themselves. (#769)
 - **Astro projects are now indexed.** `.astro` files previously weren't parsed at all — on a typical Astro site that left most of the codebase invisible to search, impact, and `codegraph_explore`. CodeGraph now extracts the TypeScript frontmatter (functions, imports, `getStaticPaths`, …) and client-side `<script>` blocks, captures function calls and `<Component>` usages in template markup so cross-component dependencies trace end-to-end, resolves the `Astro` global and `astro:*` module imports as framework-provided, and maps `src/pages/` file-based routing to route nodes (`.astro` pages and `.ts` endpoints, including `[param]` and `[...rest]` dynamic segments, with underscore-prefixed files correctly excluded). Validated on two real-world Astro sites with 93% measured cross-file coverage and every page mapping to its route. Thanks @xingwangzhe. (#768) (Astro)

+ 7 - 5
README.md

@@ -326,7 +326,7 @@ The installer will:
 - Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**
 - Prompt to install `codegraph` on your PATH (so agents can launch the MCP server)
 - Ask whether configs apply to all your projects or just this one
-- Write each chosen agent's MCP server config (the codegraph usage guide is delivered by the MCP server itself, so no instructions file is added to `CLAUDE.md` / `AGENTS.md` / etc.)
+- Write each chosen agent's MCP server config, plus a small marker-fenced CodeGraph section in the agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) — that's how subagents and non-MCP agents learn the `codegraph explore` / `codegraph node` commands, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
 - Set up auto-allow permissions when Claude Code is one of the targets
 - Initialize your current project (local installs only)
 
@@ -406,14 +406,14 @@ npm install -g @colbymchenry/codegraph
 <details>
 <summary><strong>Agent Tool Guidance</strong></summary>
 
-CodeGraph's MCP server delivers its usage guidance to your agent **automatically**, in the MCP `initialize` response — there's no instructions file to manage and nothing is added to your `CLAUDE.md` / `AGENTS.md` / `GEMINI.md`. In short, it tells the agent to:
+CodeGraph's MCP server delivers its usage guidance to your agent **automatically**, in the MCP `initialize` response. In short, it tells the agent to:
 
 - **Answer structural questions directly with CodeGraph** — it *is* the pre-built index, so a grep/read loop just repeats work it already did. Treat the returned source as already read.
-- **Pick the tool by intent:** `codegraph_explore` for almost anything — "how does X work", a flow/"how does X reach Y", or surveying an area (one call returns the relevant symbols' source grouped by file); `codegraph_search` to just locate a symbol; `codegraph_callers`/`codegraph_callees` to walk call flow; `codegraph_impact` before editing; `codegraph_node` for one specific symbol's full source (it returns every overload for an ambiguous name).
+- **Pick the tool by intent:** `codegraph_explore` for almost anything — "how does X work", a flow/"how does X reach Y", or surveying an area (one call returns the relevant symbols' source grouped by file); `codegraph_search` to just locate a symbol; `codegraph_callers` for every call site (including callback registrations); `codegraph_node` for one symbol's full source + callers, or to read a file like the Read tool.
 - **Trust the results — don't re-verify with grep**, and check the staleness banner after edits.
-- If `.codegraph/` doesn't exist yet, offer to run `codegraph init -i`.
+- In a workspace with no index, CodeGraph announces itself inactive and serves no tools — indexing stays your decision.
 
-The exact text is `src/mcp/server-instructions.ts` — the single source of truth.
+The exact text is `src/mcp/server-instructions.ts` — the single source of truth for the main agent. Because subagents and non-MCP harnesses never see the MCP guidance, the installer also writes a four-line marker-fenced section into the agent's instructions file pointing at the `codegraph explore` / `codegraph node` CLI equivalents.
 
 </details>
 
@@ -464,6 +464,8 @@ codegraph index [path]            # Full index (--force to re-index, --quiet for
 codegraph sync [path]             # Incremental update
 codegraph status [path]           # Show statistics
 codegraph query <search>          # Search symbols (--kind, --limit, --json)
+codegraph explore <query>         # Relevant symbols' source + call paths in one shot (same output as the codegraph_explore MCP tool)
+codegraph node <symbol|file>      # One symbol's source + callers, or read a file with line numbers (same output as codegraph_node)
 codegraph files [path]            # Show file structure (--format, --filter, --max-depth, --json)
 codegraph callers <symbol>        # Find what calls a function/method (--limit, --json)
 codegraph callees <symbol>        # Find what a function/method calls (--limit, --json)

+ 41 - 30
__tests__/installer-targets.test.ts

@@ -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)', () => {

+ 97 - 0
src/bin/codegraph.ts

@@ -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]
  */

+ 33 - 11
src/installer/instructions-template.ts

@@ -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}`;

+ 7 - 9
src/installer/targets/claude.ts

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

+ 5 - 5
src/installer/targets/codex.ts

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

+ 5 - 5
src/installer/targets/gemini.ts

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

+ 5 - 5
src/installer/targets/opencode.ts

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

+ 25 - 0
src/installer/targets/shared.ts

@@ -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