* feat(mcp): steer agents to codegraph during implementation, not just Q&A Two changes targeting agents that reach for Read during edits instead of codegraph: 1. Reframe the agent-facing steering (server-instructions + codegraph_node/explore descriptions): drop "consult BEFORE ... not during"; position codegraph_node as the Read upgrade for a named symbol (verbatim current on-disk source, safe to Edit from, + caller/callee trail), explore PRIMARY / node SECONDARY, with the "cached intelligence — better context, fewer tokens" framing. 2. File-view mode: codegraph_node now accepts a `file` with no `symbol` and returns that file's symbol map + graph role (its dependents), plus verbatim bodies with includeCode — so it can displace a path-keyed Read, not just a symbol lookup. Resolves a path or basename; dedups nested members; budget-capped. To be A/B'd on an implementation task before shipping (per the retrieval doctrine: steering changes must be measured, not assumed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note codegraph_node file-view + implementation steering --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
/**
|
|
* codegraph_node FILE-VIEW mode: a bare `file` (no `symbol`) returns that file's
|
|
* symbol map + graph role (dependents), and verbatim bodies with includeCode —
|
|
* a Read replacement for a source file that also surfaces the blast radius.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import CodeGraph from '../src/index';
|
|
import { ToolHandler } from '../src/mcp/tools';
|
|
|
|
describe('codegraph_node file-view (Read replacement)', () => {
|
|
let dir: string;
|
|
let cg: CodeGraph;
|
|
let h: ToolHandler;
|
|
|
|
beforeEach(async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fileview-'));
|
|
fs.mkdirSync(path.join(dir, 'src'));
|
|
fs.writeFileSync(
|
|
path.join(dir, 'src', 'a.ts'),
|
|
'export function helper(x: number) {\n return x + 1;\n}\nexport class Widget {\n build() { return helper(1); }\n}\n',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(dir, 'src', 'b.ts'),
|
|
"import { helper } from './a';\nexport function useHelper() { return helper(2); }\n",
|
|
);
|
|
cg = CodeGraph.initSync(dir, { config: { include: ['**/*.ts'], exclude: [] } });
|
|
await cg.indexAll();
|
|
h = new ToolHandler(cg);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (cg) cg.close();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const text = async (args: Record<string, unknown>): Promise<string> =>
|
|
(await h.execute('codegraph_node', args)).content.map((c) => c.text).join('\n');
|
|
|
|
it("a bare file (no symbol) returns the file's symbols + dependents", async () => {
|
|
const out = await text({ file: 'a.ts' });
|
|
expect(out).toContain('src/a.ts');
|
|
expect(out).toContain('helper');
|
|
expect(out).toContain('Widget');
|
|
expect(out).toMatch(/depended on by 1 file/i);
|
|
expect(out).toContain('src/b.ts'); // the dependent file (blast radius)
|
|
});
|
|
|
|
it('resolves by basename and returns verbatim bodies with includeCode', async () => {
|
|
const out = await text({ file: 'a.ts', includeCode: true });
|
|
expect(out).toContain('return x + 1'); // helper body
|
|
expect(out).toContain('class Widget'); // class body, verbatim
|
|
// It must NOT steer the agent back to Read — it is the Read replacement.
|
|
expect(out.toLowerCase()).not.toContain('read `src/a.ts`');
|
|
});
|
|
|
|
it('still works as a normal symbol lookup (no regression)', async () => {
|
|
const out = await text({ symbol: 'helper', includeCode: true });
|
|
expect(out).toContain('helper');
|
|
expect(out).toContain('return x + 1');
|
|
});
|
|
|
|
it('a miss returns a helpful message, not a crash', async () => {
|
|
const out = await text({ file: 'does-not-exist.ts' });
|
|
expect(out).toMatch(/no indexed file matches/i);
|
|
});
|
|
});
|