The MCP server gated tool availability on whether the server root had a .codegraph/ index, so in a monorepo where only sub-projects are indexed the agent saw zero tools — and couldn't reach an indexed sub-project even by projectPath. A session started before `codegraph init` also never surfaced the tools afterward. The Claude front-load hook had the mirror gap: it only walked UP for an index, so it stayed silent at a monorepo root. MCP server: - Always expose the tool surface; when the root isn't indexed, send a per-project instructions variant (pass projectPath) instead of the "inactive" note. Safety comes from response SHAPE (success-shaped guidance, never isError), not from hiding tools. - Reword the no-default-project guidance to be per-project, not per-session, and sharpen the projectPath schema description. Front-load hook (UserPromptSubmit): - Scan DOWN (bounded depth, workspace-root-gated) for indexed sub-projects and shape the injection by topology: front-load the one the prompt names, nudge about the rest, or list them when ambiguous. Verified: full suite (1703 passed); a live two-package monorepo run confirms the hook front-loads the correct sub-project with no cross-package leakage. The front-load's net speed effect is the existing multi-file-vs-single-file tradeoff, unchanged by this work. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0a91d0f512
commit
85a8f32fd9
+49
-27
@@ -26,7 +26,7 @@
|
||||
import { Command } from 'commander';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot } from '../directory';
|
||||
import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload } from '../directory';
|
||||
import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree';
|
||||
import { createShimmerProgress } from '../ui/shimmer-progress';
|
||||
import { getGlyphs } from '../ui/glyphs';
|
||||
@@ -1059,34 +1059,56 @@ program
|
||||
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(?:es|ed)?|call(?:s|ed|er|ers|ee)?|depend|impact|affect|wired?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
|
||||
if (!prompt || !STRUCTURAL.test(prompt)) return;
|
||||
|
||||
// Find an indexed project: cwd, then walk up a few levels.
|
||||
let root: string | null = null;
|
||||
let dir = path.resolve(String(input.cwd || process.cwd()));
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (isInitialized(dir)) { root = dir; break; }
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
if (!root) return; // not indexed — the agent's normal tools apply
|
||||
// Decide what to inject, shaped by WHERE the index(es) are: the nearest
|
||||
// indexed ancestor of cwd, or — when cwd is an un-indexed workspace root
|
||||
// whose indexed project(s) live in sub-dirs (the monorepo case, #964) —
|
||||
// the sub-project the prompt points at, plus a `projectPath` nudge for any
|
||||
// others. Without the down-scan the hook injected nothing at a monorepo
|
||||
// root (it only walked up), so the validated adoption lever never fired
|
||||
// exactly where the agent most needs it.
|
||||
const plan = planFrontload(String(input.cwd || process.cwd()), prompt);
|
||||
if (!plan.exploreRoot && plan.nudgeProjects.length === 0) return; // nothing reachable — the agent's normal tools apply
|
||||
|
||||
const { default: CodeGraph } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.open(root);
|
||||
try {
|
||||
const { ToolHandler } = await import('../mcp/tools');
|
||||
const handler = new ToolHandler(cg);
|
||||
const result = await handler.execute('codegraph_explore', { query: prompt });
|
||||
const text = result.content[0]?.text ?? '';
|
||||
if (!result.isError && text.trim()) {
|
||||
// Cap the injection so a large-repo explore can't flood the prompt.
|
||||
const MAX = 16000;
|
||||
const body = text.length > MAX ? `${text.slice(0, MAX)}\n…(truncated; call codegraph_explore for the rest)` : text;
|
||||
process.stdout.write(
|
||||
`<codegraph_context note="Structural context from CodeGraph for this prompt — treat returned source as already read; call codegraph_explore for more.">\n${body}\n</codegraph_context>\n`,
|
||||
);
|
||||
// A "pass projectPath" line for indexed sub-projects we did NOT front-load.
|
||||
// Follow-up codegraph_explore calls against a sub-project (cwd isn't its
|
||||
// index root) need an explicit projectPath, so spell it out.
|
||||
const nudge = (projects: string[], lead: string): string =>
|
||||
`${lead}\n${projects.map((p) => ` - projectPath: "${p}"`).join('\n')}\n`;
|
||||
|
||||
if (plan.exploreRoot) {
|
||||
const { default: CodeGraph } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.open(plan.exploreRoot);
|
||||
try {
|
||||
const { ToolHandler } = await import('../mcp/tools');
|
||||
const handler = new ToolHandler(cg);
|
||||
const result = await handler.execute('codegraph_explore', { query: prompt });
|
||||
const text = result.content[0]?.text ?? '';
|
||||
if (!result.isError && text.trim()) {
|
||||
// Cap the injection so a large-repo explore can't flood the prompt.
|
||||
const MAX = 16000;
|
||||
const body = text.length > MAX ? `${text.slice(0, MAX)}\n…(truncated; call codegraph_explore for the rest)` : text;
|
||||
// For a front-loaded SUB-project, a follow-up explore needs its path.
|
||||
const more = plan.viaSubScan
|
||||
? `call codegraph_explore with projectPath: "${plan.exploreRoot}" for more`
|
||||
: 'call codegraph_explore for more';
|
||||
const others = plan.nudgeProjects.length
|
||||
? `\n${nudge(plan.nudgeProjects, 'Other indexed projects in this workspace — pass projectPath to query them:')}`
|
||||
: '';
|
||||
process.stdout.write(
|
||||
`<codegraph_context note="Structural context from CodeGraph for this prompt — treat returned source as already read; ${more}.">\n${body}${others}\n</codegraph_context>\n`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
cg.destroy();
|
||||
}
|
||||
} finally {
|
||||
cg.destroy();
|
||||
} else {
|
||||
// Several indexed sub-projects, none a clear match — don't guess; tell
|
||||
// the agent they exist and how to query one.
|
||||
process.stdout.write(
|
||||
`<codegraph_context note="CodeGraph is available for this workspace's indexed sub-projects — query one by passing projectPath to codegraph_explore.">\n` +
|
||||
nudge(plan.nudgeProjects, "This workspace's CodeGraph indexes live in sub-projects. To use CodeGraph, call codegraph_explore with the projectPath of the relevant one:") +
|
||||
`</codegraph_context>\n`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Degradable by contract: never surface an error to the prompt pipeline.
|
||||
|
||||
Reference in New Issue
Block a user