diff --git a/internal/boot/boot.go b/internal/boot/boot.go index 15e9963ad..93d812611 100644 --- a/internal/boot/boot.go +++ b/internal/boot/boot.go @@ -245,6 +245,7 @@ func Build(ctx context.Context, opts Options) (*control.Controller, error) { case ok: spec := plugin.Spec{ Name: "codegraph", + StripRawPrefix: "codegraph_", Command: bin, Args: []string{"serve", "--mcp"}, Dir: root, @@ -345,6 +346,24 @@ func Build(ctx context.Context, opts Options) (*control.Controller, error) { registerDeferred(lazySpecs, false) registerDeferred(bgSpecs, true) + // Inject codegraph steering into the system prompt when symbol-graph tools + // are available, so the model knows to prefer them for architecture / call-graph + // questions over grep/read_file. Also register codegraph tool names in the + // subagent allowed-tools list so explore/research/review can use them. + if cfg.Codegraph.Enabled { + prefix := plugin.ToolPrefix("codegraph") + var cgTools []string + for _, name := range reg.Names() { + if strings.HasPrefix(name, prefix) { + cgTools = append(cgTools, name) + } + } + if len(cgTools) > 0 { + sysPrompt += "\n\n" + codegraph.SteerText + skill.SetExtraReadTools(cgTools) + } + } + for _, msg := range demoteMessages { sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: msg}) } diff --git a/internal/codegraph/codegraph.go b/internal/codegraph/codegraph.go index 4f707683a..350df68da 100644 --- a/internal/codegraph/codegraph.go +++ b/internal/codegraph/codegraph.go @@ -28,6 +28,18 @@ import ( const initTimeout = 30 * time.Second +// SteerText is injected into the system prompt when CodeGraph tools are +// available, so the model knows to prefer them for symbol-level questions. +const SteerText = `## Code Intelligence (codegraph) +You have codegraph tools for symbol-level code intelligence. For architecture questions, "how does X work", call graphs, symbol search, and impact analysis, prefer codegraph tools over grep/read_file: +- codegraph_context — entry points + related symbols + key code in one call (USE THIS FIRST for "how does X work") +- codegraph_search — find symbols by name (functions, types, interfaces) +- codegraph_callers / codegraph_callees — trace call chains +- codegraph_impact — what breaks if I change X +- codegraph_trace — full call path between two symbols +- codegraph_files — project file tree with symbol counts +Use grep/read_file for content search (comments, strings, config values) and when codegraph is not available.` + // BundleDirName is the directory, beside the reasonix executable, that the release // archive unpacks the CodeGraph bundle into. Its launcher lives at // /bin/codegraph, with the bundled node runtime and lib/ beside it; diff --git a/internal/plugin/lazy.go b/internal/plugin/lazy.go index 7e9cdb383..9abd76f7e 100644 --- a/internal/plugin/lazy.go +++ b/internal/plugin/lazy.go @@ -15,6 +15,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" "time" @@ -241,9 +242,13 @@ func LazyToolset(spec Spec, cs *CachedSchema, host *Host, reg *tool.Registry, se } else { out = make([]tool.Tool, 0, len(cs.Tools)) for _, ct := range cs.Tools { + visibleName := ct.Name + if spec.StripRawPrefix != "" { + visibleName = strings.TrimPrefix(visibleName, spec.StripRawPrefix) + } out = append(out, &lazyTool{ shared: shared, - name: toolName(spec.Name, ct.Name), + name: toolName(spec.Name, visibleName), desc: ct.Description, schema: ct.Schema, readOnly: spec.toolReadOnly(ct.Name, ct.ReadOnly), diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 3877a932c..51f7577b9 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -50,6 +50,12 @@ type Spec struct { // the server omits annotations.readOnlyHint. It is for first-party adapters // with known semantics; user-configured plugins should rely on MCP metadata. ReadOnlyToolNames map[string]bool + // StripRawPrefix, when non-empty, removes this prefix from each MCP tool's + // raw name before namespacing. For example, StripRawPrefix="codegraph_" turns + // "codegraph_context" into "context", yielding "mcp__codegraph__context" + // instead of the redundant "mcp__codegraph__codegraph_context". The original + // raw name is preserved for MCP protocol calls. + StripRawPrefix string } // transport carries JSON-RPC messages to and from one MCP server. call sends a @@ -744,10 +750,14 @@ func (c *Client) listTools(ctx context.Context) ([]tool.Tool, error) { tools := make([]tool.Tool, 0, len(out.Tools)) for _, t := range out.Tools { hinted := t.Annotations != nil && t.Annotations.ReadOnlyHint + visibleName := t.Name + if c.spec.StripRawPrefix != "" { + visibleName = strings.TrimPrefix(visibleName, c.spec.StripRawPrefix) + } toolInfos = append(toolInfos, ToolInfo{Name: t.Name, Description: t.Description}) tools = append(tools, &remoteTool{ client: c, - name: toolName(c.name, t.Name), + name: toolName(c.name, visibleName), rawName: t.Name, desc: t.Description, schema: canonicalizeSchema(t.InputSchema), diff --git a/internal/skill/builtins.go b/internal/skill/builtins.go index 644fd7a91..c377555b8 100644 --- a/internal/skill/builtins.go +++ b/internal/skill/builtins.go @@ -14,9 +14,10 @@ const tuiFormatting = `Keep the final answer compact and terminal-friendly: shor const builtinExploreBody = `You are running as an exploration subagent. Investigate the codebase the parent pointed you at, then return one focused, distilled answer. How to operate: -- Use read_file, grep, glob, ls as your primary tools. Stay read-only. -- For "find all places that call / reference / use X" questions, use ` + "`grep`" + ` (content search) — NOT ` + "`glob`" + ` (which only matches file names). Using the wrong one gives empty results and wastes your budget. -- Cast a wide net first (grep for symbol references, ls/glob for structure) to map the territory; then read the 3-10 most relevant files in full. +- Use codegraph tools (codegraph_context, codegraph_search, codegraph_callers, codegraph_callees, codegraph_trace) as your PRIMARY tools for symbol/code-structure questions. Fall back to read_file, grep, glob, ls for content search (comments, strings, config) or when codegraph tools are not available. Stay read-only. +- codegraph_context is the best starting point for "how does X work" / architecture questions — it returns entry points + related symbols + key code in one call. +- For "find all places that call / reference / use X" questions: use codegraph_callers (preferred) or ` + "`grep`" + ` (content search) — NOT ` + "`glob`" + ` (which only matches file names). Using the wrong one gives empty results and wastes your budget. +- Cast a wide net first (codegraph_search for symbols, grep for content references, ls/glob for structure) to map the territory; then read the 3-10 most relevant files in full. - Don't read every file — be selective. Breadth on the first pass, depth only where the question demands it. - Stop exploring as soon as you can answer. The parent doesn't see your tool calls, so over-exploration is pure waste. @@ -34,8 +35,9 @@ The 'task' the parent gave you is the question you must answer. Treat any other const builtinResearchBody = `You are running as a research subagent. Gather information from code AND the web, synthesize it, and return one focused conclusion. How to operate: -- Combine code reading (read_file, grep, glob) with web_fetch as appropriate. (There is no dedicated web-search tool — fetch the canonical doc/spec URL directly when you know it.) -- For "how does X work" / "is Y supported" questions: fetch the canonical reference, then verify against the local code. +- Combine code reading (codegraph tools + read_file, grep, glob) with web_fetch as appropriate. (There is no dedicated web-search tool — fetch the canonical doc/spec URL directly when you know it.) +- For "how does X work" questions: use codegraph_context first for symbol-level understanding, then read_file for full context. +- For "is Y supported" questions: fetch the canonical reference, then verify against the local code. - For "what's our policy on Z" / "where do we use Q": local code first, web only to compare against external standards. - Cap yourself at ~10 tool calls. If you can't converge, return what you have plus a note on what's missing. @@ -57,7 +59,7 @@ How to operate: - Default scope: the current branch's diff vs the default branch. If the task names a specific commit range or files, honor that instead. - Discover scope first: ` + "`bash git status`" + `, ` + "`git diff --stat`" + `, ` + "`git log --oneline`" + `. Then ` + "`git diff`" + ` (or ` + "`git diff ...HEAD`" + `) for the hunks. - Read touched files (read_file) when the diff alone lacks context — signatures, surrounding invariants, callers. -- For "any callers depending on this?" questions: grep the symbol BEFORE asserting impact. +- For "any callers depending on this?" questions: use codegraph_callers or codegraph_impact (preferred) or grep the symbol BEFORE asserting impact. - Stay read-only. Never commit, never write files, never propose edits as applied changes. The parent decides whether to act. - Cap yourself at ~12 tool calls. If the diff is too big, pick the riskiest 2-3 files and say so. @@ -85,7 +87,7 @@ const builtinSecurityReviewBody = `You are running as a security-review subagent How to operate: - Default scope: the current branch's diff vs the default branch. Honor a named range or directory if given. - Discover scope first: ` + "`bash git status`" + `, ` + "`git diff --stat`" + `, ` + "`git diff ...HEAD`" + `. Read touched files (read_file) when the diff lacks security context — auth checks, input validation, the handler that calls the changed code. -- Use grep to verify "is this user-controlled input ever sanitized later?" / "what other call sites depend on this validation?" before asserting impact. +- Use codegraph_callers or codegraph_impact (preferred) or grep to verify "is this user-controlled input ever sanitized later?" / "what other call sites depend on this validation?" before asserting impact. - Stay read-only. Never write, never run destructive commands. The parent decides what to act on. - Cap yourself at ~12 tool calls. If the diff is too big, focus on the riskiest 2-3 files and say so. @@ -148,11 +150,20 @@ Rules: - Don't fabricate conventions the code doesn't demonstrate. - After writing, summarize in one or two lines what you captured and tell the user to review and edit it.` +// extraReadTools holds additional tool names (e.g. codegraph tools) injected at +// boot time so subagent skills can use them without hardcoding MCP-prefixed names. +var extraReadTools []string + +// SetExtraReadTools registers additional read-only tool names that subagent +// skills (explore, research, review, security-review) are allowed to use. Call +// from boot after plugin tools are registered. +func SetExtraReadTools(names []string) { extraReadTools = names } + // builtinSkills returns the shipped skills. A fresh slice each call so callers // can't mutate the shared set. func builtinSkills() []Skill { - readCodeTools := []string{"read_file", "ls", "glob", "grep"} - reviewTools := []string{"read_file", "ls", "glob", "grep", "bash"} + readCodeTools := append([]string{"read_file", "ls", "glob", "grep"}, extraReadTools...) + reviewTools := append(append([]string(nil), readCodeTools...), "bash") return []Skill{ { Name: "init", diff --git a/internal/tool/builtin/bash.go b/internal/tool/builtin/bash.go index 59f29f95c..303bcba9e 100644 --- a/internal/tool/builtin/bash.go +++ b/internal/tool/builtin/bash.go @@ -53,7 +53,7 @@ func (b bash) Description() string { // bashToolSteer points the model at the cross-platform built-in tools instead of // shell utilities, so it doesn't reach for grep/cat/ls/find (absent or different // on native Windows) when a native tool already does the job everywhere. -const bashToolSteer = " Use for builds, tests, git, package managers, etc. To search/read/list/edit files, prefer the dedicated tools (grep, read_file, ls, glob, edit_file) over shell grep/cat/ls/find/sed — they behave identically on every OS." +const bashToolSteer = " Use for builds, tests, git, package managers, etc. To search/read/list/edit files, prefer the dedicated tools (grep, read_file, ls, glob, edit_file) over shell grep/cat/ls/find/sed — they behave identically on every OS. For symbol search, call graphs, or architecture questions, use codegraph tools instead of grep." // resolved returns the bound shell, resolving lazily for the zero-value instance // (e.g. a registry that never went through ConfineBash).