From 05e9fe29f5c420af19f202e3cd7b1f609d0ed966 Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 14:57:10 -0400 Subject: [PATCH 1/6] Improve agent orientation outputs --- README.md | 27 ++--- codegraph-skill/codegraph/SKILL.md | 15 ++- docs/agent-workflows.md | 26 +++-- docs/cli.md | 12 +- docs/library-api.md | 14 +-- docs/mcp.md | 2 +- src/agent.ts | 2 - src/agent/orient.ts | 178 ++++++++++++++++++++--------- src/agent/packet.ts | 38 +++--- src/cli/help.ts | 20 ++-- src/cli/options.ts | 2 +- src/cli/orient.ts | 39 +++++-- src/cli/packet.ts | 6 +- src/index.ts | 6 +- src/mcp/server.ts | 6 +- src/mcp/tools.ts | 6 +- tests/agent-orient.test.ts | 44 +++++-- tests/agent-packet.test.ts | 26 ++--- tests/cli-command-modules.test.ts | 12 +- tests/cli-regressions.test.ts | 23 ++-- tests/mcp-server.test.ts | 7 +- tests/package-metadata.test.ts | 2 +- 22 files changed, 311 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index 480a0a7d..135daff5 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ Use Codegraph when you need fast structural answers about a repo without relying - Export graph data as JSON, Mermaid, DOT, or SQLite, then inspect it from scripts or the browser graph viewer app. - Keep one workflow across source languages, monorepos, and graph-first document and template formats instead of stitching together separate tools. -For a first pass, run `doctor`, then `orient --root . --budget small --json`. -Use `inspect`, `search`, `packet get`, `impact`, and `review` as follow-ups when you need deeper architecture, symbol, or change context. +For a first pass, run `orient --root . --budget small --pretty`. +Use `packet get`, `search`, `explain`, `impact`, and `review` from the recommended next commands when you need deeper architecture, symbol, or change context. +For PR, worktree, or sweeping review tasks, start with `review --base HEAD --head WORKTREE --summary` or `impact --base HEAD --head WORKTREE --pretty`. Detailed command contracts and JSON shapes live in [docs/cli.md](./docs/cli.md). ## Features @@ -37,7 +38,7 @@ Detailed command contracts and JSON shapes live in [docs/cli.md](./docs/cli.md). - Multi-language dependency graphs, including imports, re-exports, `require()`, dynamic imports, workspace resolution, document links, stylesheet imports, and SFC script dependencies. - Per-file symbol indexes with locals, exports, docstrings, line spans, and lightweight complexity metadata. - Cross-file go-to-definition and find-references support across the shared source-language pipeline. -- Deterministic agent orientation, packet retrieval, search, bounded explanations, portable artifact bundles, and MCP tools across files, symbols, chunks, SQL objects, graph neighborhoods, and review ranges with stable follow-up handles. +- Deterministic agent orientation, packet retrieval, search, bounded explanations, portable artifact bundles, and MCP tools across files, symbols, chunks, SQL objects, graph neighborhoods, and review ranges with stable follow-up targets. - Semantic chunking for code and text files, including Vue and Svelte single-file component block splitting. - Duplicate and near-duplicate detection over indexed symbols, semantic chunks, text chunks, token fingerprints, and AST shape hashes when parser context is available. - AST grep, public API summaries, unresolved import reports, hotspot analysis, cycle detection, and shortest dependency paths. @@ -76,22 +77,22 @@ npm run build `npm run build` always rebuilds `dist/`. If Cargo is available, it also requires the local native workspace build to succeed; if Cargo is unavailable, it still completes with the JavaScript build output and a warning. -Then start with orientation and follow the returned handles or commands: +Then start with orientation and follow the returned commands: ```bash -# confirm runtime and artifact state -node ./dist/cli.js doctor - # initial repo orientation with next-step suggestions -node ./dist/cli.js orient --root . --budget small --json +node ./dist/cli.js orient --root . --budget small --pretty + +# optional runtime and artifact health check +node ./dist/cli.js doctor # optional broader architecture summary node ./dist/cli.js inspect ./src --limit 20 # find and explain a concrete anchor -node ./dist/cli.js packet get --json +node ./dist/cli.js packet get src/cli.ts --pretty node ./dist/cli.js search "graph json" --json -node ./dist/cli.js explain src/cli.ts --json +node ./dist/cli.js explain src/cli.ts # build a graph for product code node ./dist/cli.js graph --root . ./src --compact-json --output codegraph.json @@ -122,10 +123,10 @@ Use these as starting points, then see [docs/cli.md](./docs/cli.md) for all flag ```bash # repo orientation and bounded follow-up -codegraph orient --root . --budget small --json -codegraph packet get --json +codegraph orient --root . --budget small --pretty +codegraph packet get src/cli/graph.ts --pretty codegraph search "graph json" --json -codegraph explain file:src/cli/graph.ts --json +codegraph explain file:src/cli/graph.ts # semantic navigation codegraph goto diff --git a/codegraph-skill/codegraph/SKILL.md b/codegraph-skill/codegraph/SKILL.md index 9d5c3b9b..d96e3663 100644 --- a/codegraph-skill/codegraph/SKILL.md +++ b/codegraph-skill/codegraph/SKILL.md @@ -21,15 +21,18 @@ Do not use Codegraph as the only evidence for runtime behavior; pair it with tes Start bounded: ```bash -codegraph doctor -codegraph orient --root . --budget small --json +codegraph orient --root . --budget small --pretty ``` +Use `doctor` only when install, native-runtime, or artifact health is the task. + +For PR, worktree, or sweeping review tasks, start with `codegraph review --base HEAD --head WORKTREE --summary` or `codegraph impact --base HEAD --head WORKTREE --pretty` instead. + Then choose the smallest useful follow-up: -- packet: `codegraph packet get --json` +- packet: `codegraph packet get --pretty` - search: `codegraph search "auth user" --json` -- explain: `codegraph explain --json` +- explain: `codegraph explain ` - architecture: `codegraph inspect ./src --limit 20` - dependencies: `codegraph deps ` or `codegraph rdeps ` - path: `codegraph path ` @@ -47,11 +50,11 @@ For `orient`, `drift`, and positional graph commands, positional paths are inclu ## Output Choice Use readable output when a human or model will read the result. -Use JSON when the next step needs exact fields, handles, counts, or filtering. +Use JSON when the next step needs exact fields, counts, or filtering. Current high-value surfaces: -- `orient --json`: stable handles and omission counts +- `orient --pretty`: ranked first-turn focus targets with copyable follow-ups - `impact --pretty`: ranked "what could this break?" map - `review --summary`: compact reviewer handoff - `duplicates --profile cleanup`: refactor ROI ordering diff --git a/docs/agent-workflows.md b/docs/agent-workflows.md index 39d5e127..363f4cae 100644 --- a/docs/agent-workflows.md +++ b/docs/agent-workflows.md @@ -6,21 +6,23 @@ Use Codegraph for structural repo questions: architecture, dependency direction, ## Start here -For an unfamiliar repo, keep the first loop bounded: +For an unfamiliar repo, keep the first loop bounded and actionable: ```bash -codegraph doctor -codegraph orient --root . --budget small --json -codegraph packet get --json +codegraph orient --root . --budget small --pretty +codegraph packet get --pretty ``` +For PR, worktree, or sweeping review tasks, start with `codegraph review --base HEAD --head WORKTREE --summary` or `codegraph impact --base HEAD --head WORKTREE --pretty` instead of orientation. + +Use `doctor` only when package/runtime state or an existing artifact path is the question. Use `search` when the agent has a query but no handle, `explain` when it already knows a file/symbol/SQL object/handle, and `inspect` for a human-readable architecture summary. Use `artifact build` for durable handoff directories and `mcp serve` when repeated follow-up calls should share one warm repo session. Choose output by the next consumer: - Use `--pretty` or `--summary` when the next consumer is a person or language model reading the result. -- Use `--json`, MCP tools, or library APIs when the next step needs exact handles, ranges, schema fields, or filtering. +- Use `--json`, MCP tools, or library APIs when the next step needs exact fields, ranges, schema fields, or filtering. - Do not parse pretty text to recover fields already present in structured output. For durable repo-local scan scope, add `codegraph.config.json` at the project root. `discovery.ignoreGlobs` keeps large fixture, generated, or vendored folders out of agent search, MCP sessions, graphing, unresolved-import checks, impact, and review unless a command explicitly changes scan scope. @@ -32,19 +34,19 @@ For raw command flags and output contracts, see [docs/cli.md](./cli.md). For lib Start with `orient` when an agent needs compact repo context without flooding the first prompt: ```bash -codegraph orient --root . --budget small --json -codegraph orient --root . ./src --budget medium --pretty -codegraph packet get file:src%2Fcli.ts --json -codegraph packet get --max-symbols 25 --json +codegraph orient --root . --budget small --pretty +codegraph orient --root . ./src --budget medium --json +codegraph packet get src/cli.ts --pretty +codegraph packet get --max-symbols 25 --json ``` -Orientation returns summary bullets, a bounded tree, hotspot modules, budgeted health counts, stable packet handles, omitted counts, and recommended next commands. -Use `orient --pretty` for compact model-readable triage and `orient --json` when follow-up tools need exact handles or omission counts. +Orientation returns summary bullets, ranked `focus` targets, a bounded tree, budgeted health counts, omitted counts, and recommended next commands. +Use `orient --pretty` or MCP `orient` for compact model-readable triage and `orient --json` when follow-up tools need exact focus reasons, limits, or omission counts. Small orientation packets default to cheap health analysis; use larger budgets only when cycle, unresolved-import, or duplicate counts matter. ## Search anchors -Use `search` when an agent has a query but no packet handle and needs a compact starting point before calling `goto`, `refs`, `deps`, `rdeps`, `chunk`, or later explanation tooling: +Use `search` when an agent has a query but no file target or search handle and needs a compact starting point before calling `goto`, `refs`, `deps`, `rdeps`, `chunk`, or later explanation tooling: ```bash codegraph search "validate user" --json diff --git a/docs/cli.md b/docs/cli.md index 69144134..09d910fc 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -114,9 +114,9 @@ codegraph index --threads 8 --cache disk codegraph index --workers --threads 8 --cache disk # Search for agent-ready anchors across symbols, paths, chunks, SQL objects, and graph context -codegraph orient --root . --budget small --json -codegraph orient --root . ./src --budget medium --pretty -codegraph packet get --json +codegraph orient --root . --budget small --pretty +codegraph orient --root . ./src --budget medium --json +codegraph packet get src/cli.ts --pretty codegraph search "validate user" --json codegraph search "public users" --mode sql --json codegraph search "handle login" --from src/auth.ts --mode graph --depth 1 --json @@ -227,10 +227,10 @@ Short JSON shape: #### Agent orientation and packets -- Use `orient --pretty` as the compact first-turn reading surface for people or models. -- Use `orient --json` when follow-up tools need exact handles, limits, and omitted counts. +- Use `orient --pretty` as the compact first-turn reading surface for people or models; it prints the ranked `focus` targets and their follow-up commands before the scope sketch. +- Use `orient --json` when follow-up tools need exact focus reasons, limits, and omitted counts. Orient suppresses index rebuild warnings so stdout stays parseable. - Small orientation budgets default to `--health skip`. Medium and large default to `--health summary`, which counts cycles and unresolved imports while omitting duplicate health; use `--health full` when exhaustive duplicate counts matter. -- Use `packet get` with file, symbol, chunk, SQL, graph, or review handles to retrieve bounded evidence plus follow-up commands. +- Use `packet get` with file paths, symbol handles, chunk handles, SQL handles, graph handles, or review handles to retrieve bounded evidence plus follow-up commands. - Agent commands reuse the incremental index path and default to disk cache. Use shared index flags such as `--cache`, `--cache-strict`, `--cache-verify`, `--threads`, `--native`, `--workers`, `--include-glob`, `--ignore-glob`, and `--no-gitignore` when the packet should match a specific scan mode. `search` is deterministic and vectorless. `explain` resolves file paths, symbol names, SQL object names, and search handles into bounded packets with symbols, graph context, references, snippets, duplicate context, SQL facts, review tasks, candidate tests, limits, omissions, and follow-ups. Use `--max-duplicates` to tune duplicate context in `explain` and `packet get`; duplicate context also uses an internal pair budget and reports skipped duplicate work through omission counts. diff --git a/docs/library-api.md b/docs/library-api.md index 16e648df..b6e19d16 100644 --- a/docs/library-api.md +++ b/docs/library-api.md @@ -80,7 +80,7 @@ compatibility exports. ## Agent packets -`orientCodegraph()` returns a compact first-turn packet for an agent, and `getCodegraphPacket()` retrieves bounded evidence by stable handle: +`orientCodegraph()` returns compact first-turn context for an agent, and `getCodegraphPacket()` retrieves bounded evidence by file path or stable target: ```ts import { getCodegraphPacket, orientCodegraph } from "@lzehrung/codegraph"; @@ -91,18 +91,18 @@ const orientation = await orientCodegraph({ budget: "small", }); -const handle = orientation.handles[0]?.handle; -if (handle) { +const target = orientation.focus.find((entry) => entry.file); +if (target?.file) { const packet = await getCodegraphPacket({ root: process.cwd(), - handle, + target: target.file, maxSymbols: 25, }); - console.log(packet.kind, packet.followUps); + console.log(target.why, packet.kind, packet.followUps); } ``` -Use orientation before broad search when a caller needs repo context but has no query yet. Packet handles cover files, symbols, chunks, SQL objects, graph neighborhoods, and review ranges. +Use orientation before broad search when a caller needs repo context but has no query yet. `focus` ranks file targets that should be tried first, with graph-central hotspots ahead of shallow root files. Search and explain still expose stable handles for symbols, chunks, SQL objects, graph neighborhoods, and review ranges. Small orientation budgets default to `health: "skip"` and set health fields to `null` while recording the omission. Medium and large default to `health: "summary"`, which counts cycles and unresolved imports while omitting duplicate health. Use `health: "full"` when exhaustive duplicate counts are needed. ## Agent search @@ -170,7 +170,7 @@ const handlers = createCodegraphMcpHandlers({ const search = await handlers.search({ query: "auth user", limit: 5 }); const orient = await handlers.orient({ includeRoots: ["src"], budget: "small" }); -const packet = await handlers.packet_get({ handle: orient.handles[0]!.handle }); +const packet = await handlers.packet_get({ target: orient.focus.find((entry) => entry.file)!.file! }); const refs = await handlers.refs({ handle: search.results[0]!.handle }); const rows = await handlers.query_sqlite({ query: "select path from files", limit: 5 }); console.log(packet.kind, refs.references, rows.rows); diff --git a/docs/mcp.md b/docs/mcp.md index 118dc20a..cf2f0411 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -36,7 +36,7 @@ Use stdio for a client-owned subprocess. Use HTTP for one long-running Codegraph The server exposes the same bounded primitives as the CLI and library session layer: - `orient`: compact first-turn repo context. -- `packet_get`: bounded evidence packet by stable handle. +- `packet_get`: bounded evidence packet by file path or stable target. - `search`: deterministic ranked search across paths, symbols, chunks, SQL objects, and graph context. - `get_file`: bounded project file read. - `get_symbol`: resolve a stable search or explain handle. diff --git a/src/agent.ts b/src/agent.ts index 0a170637..96468443 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -2,13 +2,11 @@ export { createAgentSession } from "./agent/session.js"; export type { AgentProjectSnapshot, AgentSession, AgentSessionOptions } from "./agent/session.js"; export { orientCodegraph } from "./agent/orient.js"; export type { - AgentModuleSummary, AgentOrientBudget, AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, AgentPacketCommand, - AgentPacketHandle, AgentTreeEntry, } from "./agent/orient.js"; export { getCodegraphPacket } from "./agent/packet.js"; diff --git a/src/agent/orient.ts b/src/agent/orient.ts index 2df8de28..d217e7f4 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -31,19 +31,26 @@ export type AgentTreeEntry = { depth: number; }; -export type AgentModuleSummary = { +type AgentModuleSummary = { file: string; fanIn: number; fanOut: number; score: number; - handle: string; }; -export type AgentPacketHandle = { - kind: "file" | "review"; - handle: string; - label: string; +type AgentPacketRef = { + id: number; + kind: "file"; + file: string; +}; + +export type AgentOrientationFocus = { + id?: number; + kind: "hotspot" | "file" | "review"; + label?: string; file?: string; + why: string; + followUps: string[]; }; export type AgentPacketCommand = { @@ -52,23 +59,21 @@ export type AgentPacketCommand = { }; export type AgentOrientResponse = { - schemaVersion: 1; + schemaVersion: 2; root: string; budget: AgentOrientBudget; summary: string[]; + focus: AgentOrientationFocus[]; tree: AgentTreeEntry[]; - modules: AgentModuleSummary[]; health: { cycles: number | null; unresolved: number | null; duplicateGroups: number | null; }; - handles: AgentPacketHandle[]; recommendedNext: AgentPacketCommand[]; omittedCounts: { treeEntries: number; - hotspots: number; - handles: number; + focusTargets: number; healthAnalyses: number; }; }; @@ -83,9 +88,9 @@ const ORIENT_BUDGETS: Record< includeHealth: boolean; } > = { - small: { treeDepth: 2, maxTreeEntries: 80, maxHandles: 20, maxHotspots: 8, includeHealth: false }, - medium: { treeDepth: 3, maxTreeEntries: 160, maxHandles: 40, maxHotspots: 15, includeHealth: true }, - large: { treeDepth: 4, maxTreeEntries: 320, maxHandles: 80, maxHotspots: 25, includeHealth: true }, + small: { treeDepth: 2, maxTreeEntries: 25, maxHandles: 5, maxHotspots: 5, includeHealth: false }, + medium: { treeDepth: 3, maxTreeEntries: 80, maxHandles: 10, maxHotspots: 10, includeHealth: true }, + large: { treeDepth: 4, maxTreeEntries: 160, maxHandles: 15, maxHotspots: 15, includeHealth: true }, }; export async function orientCodegraph(request: AgentOrientRequest): Promise { @@ -123,43 +128,38 @@ export async function orientCodegraphWithSession( fanIn: hotspot.fanIn, fanOut: hotspot.fanOut, score: hotspot.score, - handle: formatAgentFileHandle({ file: hotspot.file }), })); - const fileHandles = scopedFiles.slice(0, limits.maxHandles).map((file) => ({ - kind: "file" as const, - handle: formatAgentFileHandle({ file }), - label: file, - file, - })); - const reviewHandles = request.review ? [buildReviewPacketHandle(request.review.base, request.review.head)] : []; + const packets = buildFilePackets( + scopedFiles, + modules.map((module) => module.file), + limits.maxHandles, + ); + const focus = buildFocusTargets(modules, packets, request.review); const healthMode = request.health ?? (limits.includeHealth ? "summary" : "skip"); const health = await buildHealth(root, snapshot.index, scopedAbsoluteFiles, scopedFileGraph, healthMode); - const handles = [...reviewHandles, ...fileHandles]; - const recommendedNext = buildRecommendedNext(scopedFiles, includeRoots, handles); + const recommendedNext = buildRecommendedNext(includeRoots, focus); const healthSummary = formatHealthSummary(health); return { - schemaVersion: 1, + schemaVersion: 2, root, budget, summary: [ `${scopedFiles.length} file(s) in scope.`, - `${modules.length} hotspot module(s) surfaced for follow-up.`, + `${modules.length} graph-central module(s) ranked for first follow-up.`, healthSummary, ], + focus, tree: boundedTree, - modules, health: { cycles: health.cycles, unresolved: health.unresolved, duplicateGroups: health.duplicateGroups, }, - handles, recommendedNext, omittedCounts: { treeEntries: Math.max(0, tree.length - boundedTree.length), - hotspots: Math.max(0, scopedHotspots.length - boundedHotspots.length), - handles: Math.max(0, scopedFiles.length + reviewHandles.length - handles.length), + focusTargets: Math.max(0, scopedFiles.length - packets.length), healthAnalyses: health.omittedAnalyses, }, }; @@ -224,12 +224,15 @@ function formatHealthSummary(health: { return `${health.cycles} cycle(s), ${health.unresolved} unresolved import group(s), ${health.duplicateGroups} duplicate group(s).`; } -function buildReviewPacketHandle(base: string, head: string): AgentPacketHandle { - const handle = `review:base=${encodeURIComponent(base)};head=${encodeURIComponent(head)}`; +function buildReviewFocus(base: string, head: string): AgentOrientationFocus { return { kind: "review", - handle, label: `Review ${base}..${head}`, + why: "review range requested by the caller", + followUps: [ + `codegraph review --base ${quoteShellArg(base)} --head ${quoteShellArg(head)} --summary`, + `codegraph impact --base ${quoteShellArg(base)} --head ${quoteShellArg(head)} --pretty`, + ], }; } @@ -306,30 +309,101 @@ function parentPath(file: string): string { return file.slice(0, separator); } -function buildRecommendedNext( - scopedFiles: string[], - includeRoots: string[], - handles: AgentPacketHandle[], -): AgentPacketCommand[] { - const commands: AgentPacketCommand[] = []; - const firstHandle = handles[0]; - if (firstHandle) { - const label = firstHandle.file ?? firstHandle.label; - commands.push({ - label: `Get packet for ${label}`, - command: `codegraph packet get ${quoteShellArg(firstHandle.handle)} --json`, - }); +function buildFilePackets(scopedFiles: string[], priorityFiles: string[], maxPackets: number): AgentPacketRef[] { + if (!maxPackets) return []; + const scopedFileSet = new Set(scopedFiles); + const seen = new Set(); + const orderedFiles: string[] = []; + for (const file of priorityFiles) { + if (!scopedFileSet.has(file) || seen.has(file)) continue; + seen.add(file); + orderedFiles.push(file); + } + for (const file of scopedFiles) { + if (seen.has(file)) continue; + seen.add(file); + orderedFiles.push(file); + } + return orderedFiles.slice(0, maxPackets).map((file, index) => ({ + id: index + 1, + kind: "file" as const, + file, + })); +} + +function buildFocusTargets( + modules: AgentModuleSummary[], + packets: AgentPacketRef[], + review: AgentOrientRequest["review"] | undefined, +): AgentOrientationFocus[] { + const focus: AgentOrientationFocus[] = []; + if (review) { + focus.push(buildReviewFocus(review.base, review.head)); } - if (scopedFiles.length) { - const firstRoot = includeRoots[0] ?? "."; - commands.push({ - label: "Inspect hotspots", - command: `codegraph hotspots ${quoteShellArg(firstRoot)} --limit 20 --json`, + const modulesByFile = new Map(modules.map((module) => [module.file, module])); + for (const packet of packets) { + const module = modulesByFile.get(packet.file); + if (module) { + focus.push({ + id: packet.id, + kind: "hotspot" as const, + file: module.file, + why: `graph-central module: fan-in ${module.fanIn}, fan-out ${module.fanOut}, score ${module.score}`, + followUps: [formatPacketCommand(packet.file), `codegraph explain ${quoteShellArg(module.file)}`], + }); + continue; + } + focus.push({ + id: packet.id, + kind: "file" as const, + file: packet.file, + why: "bounded file packet candidate inside the requested scope", + followUps: [formatPacketCommand(packet.file)], }); } + return focus; +} + +function buildRecommendedNext(includeRoots: string[], focus: AgentOrientationFocus[]): AgentPacketCommand[] { + const commands: AgentPacketCommand[] = []; + const firstFocus = focus[0]; + if (firstFocus) { + for (const followUp of firstFocus.followUps) { + commands.push({ + label: labelForFollowUp(firstFocus, followUp), + command: followUp, + }); + } + } + const firstRoot = includeRoots[0] ?? "."; + commands.push({ + label: "Rank scoped hotspots", + command: `codegraph hotspots ${quoteShellArg(firstRoot)} --limit 20`, + }); commands.push({ - label: "Search for an anchor", + label: "Map current worktree impact", + command: "codegraph impact --base HEAD --head WORKTREE --pretty", + }); + commands.push({ + label: "Review current worktree", + command: "codegraph review --base HEAD --head WORKTREE --summary", + }); + commands.push({ + label: "Search for a task-specific anchor", command: "codegraph search --json", }); return commands; } + +function formatPacketCommand(file: string): string { + return `codegraph packet get ${quoteShellArg(file)} --pretty`; +} + +function labelForFollowUp(focus: AgentOrientationFocus, followUp: string): string { + const label = focus.file ?? focus.label ?? "focus target"; + if (followUp.startsWith("codegraph packet get ")) return `Get packet for ${label}`; + if (followUp.startsWith("codegraph explain ")) return `Explain ${label}`; + if (followUp.startsWith("codegraph review ")) return `Review ${label}`; + if (followUp.startsWith("codegraph impact ")) return `Map impact for ${label}`; + return label; +} diff --git a/src/agent/packet.ts b/src/agent/packet.ts index ec1ff548..ee53cc39 100644 --- a/src/agent/packet.ts +++ b/src/agent/packet.ts @@ -8,7 +8,7 @@ export type AgentPacketKind = "file" | "symbol" | "chunk" | "sql_object" | "grap export type AgentPacketRequest = { root: string; - handle: string; + target: string; buildOptions?: BuildOptions; maxSymbols?: number; maxSnippets?: number; @@ -20,7 +20,7 @@ export type AgentPacketPayload = AgentExplanation | ReviewReport; export type AgentPacketResponse = { schemaVersion: 1; root: string; - handle: string; + target: string; kind: AgentPacketKind; packet: AgentPacketPayload; limits: Record; @@ -28,10 +28,8 @@ export type AgentPacketResponse = { followUps: string[]; }; -const ACCEPTED_HANDLE_PREFIXES = ["file:", "symbol:", "chunk:", "sql:", "graph:", "review:"]; - export async function getCodegraphPacket(request: AgentPacketRequest): Promise { - const kind = requirePacketKind(request.handle); + const kind = inferPacketKind(request.target); if (kind === "review") { return await buildReviewPacket(request); } @@ -47,7 +45,7 @@ export async function getCodegraphPacketWithSession( session: AgentSession, request: AgentPacketRequest, ): Promise { - const kind = requirePacketKind(request.handle); + const kind = inferPacketKind(request.target); if (kind === "review") { return await buildReviewPacket(request); } @@ -62,7 +60,7 @@ async function buildExplainPacket( ): Promise { const explainRequest: AgentExplainTarget = { root: request.root, - target: request.handle, + target: request.target, }; if (request.maxSymbols !== undefined) { explainRequest.maxSymbols = request.maxSymbols; @@ -76,13 +74,13 @@ async function buildExplainPacket( const explanation = await explainCodegraphTargetWithSession(session, explainRequest); if (explanation.target.kind === "not_found") { - throw new Error(`Packet handle did not resolve: ${request.handle}`); + throw new Error(`Packet target did not resolve: ${request.target}`); } return { schemaVersion: 1, root: explanation.root, - handle: request.handle, + target: request.target, kind, packet: explanation, limits: explanation.limits, @@ -91,18 +89,16 @@ async function buildExplainPacket( }; } -function requirePacketKind(handle: string): AgentPacketKind { - const kind = kindForHandle(handle); - if (!kind) { - throw new Error(`Unsupported packet handle. Expected one of: ${ACCEPTED_HANDLE_PREFIXES.join(", ")}`); - } - return kind; +function inferPacketKind(target: string): AgentPacketKind { + const kind = kindForHandle(target); + if (kind) return kind; + return "file"; } async function buildReviewPacket(request: AgentPacketRequest): Promise { - const range = parseReviewHandle(request.handle); + const range = parseReviewHandle(request.target); if (!range) { - throw new Error("Invalid review packet handle. Expected review:base=;head=."); + throw new Error("Invalid review packet target. Expected review:base=;head=."); } const report = await buildReviewReport(request.root, { gitBase: range.base, @@ -112,7 +108,7 @@ async function buildReviewPacket(request: AgentPacketRequest): Promise(); - for (const part of handle.slice("review:".length).split(";")) { + for (const part of target.slice("review:".length).split(";")) { const separator = part.indexOf("="); if (separator < 0) continue; const key = part.slice(0, separator); diff --git a/src/cli/help.ts b/src/cli/help.ts index dea98b59..ddb6784b 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -6,7 +6,7 @@ Commands: graph Build dependency graph (default) inspect Summarize repo structure and recommend next commands orient Build a compact first-turn packet for agent repo context - packet Retrieve bounded evidence packets by stable handle + packet Retrieve bounded evidence packets by file path or stable target search Ranked agent search across files, symbols, chunks, SQL, and graph context explain Explain a file, symbol, SQL object, or search handle artifact Build an agent-ready SQLite/graph/report/question bundle @@ -69,10 +69,10 @@ Examples: codegraph graph --fast-graph --mermaid ./src codegraph version codegraph -v + codegraph orient ./src --budget small --pretty + codegraph packet get file:src%2Fcli.ts --json codegraph doctor codegraph inspect ./src --limit 20 - codegraph orient ./src --budget small --json - codegraph packet get file:src%2Fcli.ts --json codegraph duplicates ./src --min-confidence medium codegraph search "auth user" --json codegraph explain src/auth.ts --json @@ -154,19 +154,19 @@ export const ORIENT_HELP_TEXT = `codegraph orient - Build a compact first-turn p Usage: codegraph orient [roots...] [--root ] [--budget small|medium|large] [--health skip|summary|full] [--json | --pretty] Output: - Orientation includes summary bullets, a bounded project tree, hotspot modules, budgeted health counts, stable packet handles, omission counts, and copyable follow-up commands. - Small budget defaults to --health skip. Medium and large default to --health summary, which counts cycles and unresolved imports without duplicate detection. Use --health full for exhaustive duplicate health. + Orientation includes summary bullets, ranked focus targets with follow-up commands, a bounded project tree, budgeted health counts, and omission counts. + Use --pretty for model-readable triage and --json when tooling needs exact focus reasons, limits, or omissions. Small budget defaults to --health skip. Medium and large default to --health summary, which counts cycles and unresolved imports without duplicate detection. Index options: Supports shared --cache, --cache-strict, --cache-verify, --threads, --native, --workers, --include-glob, --ignore-glob, and --no-gitignore options. `; -export const PACKET_HELP_TEXT = `codegraph packet - Retrieve bounded evidence packets by stable handle +export const PACKET_HELP_TEXT = `codegraph packet - Retrieve bounded evidence packets by file path or stable target -Usage: codegraph packet get [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] +Usage: codegraph packet get [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] -Handles: - Accepts file:, symbol:, chunk:, sql:, graph:, and review: handles. CLI orient returns file handles; review handles are produced by library orientation calls that include a review range. +Targets: + Accepts project file paths plus symbol:, chunk:, sql:, graph:, and review: handles from search or explain output. Index options: Supports shared --cache, --cache-strict, --cache-verify, --threads, --native, --workers, --include-glob, --ignore-glob, and --no-gitignore options. @@ -235,7 +235,7 @@ Usage: codegraph mcp serve [--root ] [--artifact ] [--stdio | --port Tools: orient Build a compact first-turn repo packet - packet_get Retrieve bounded evidence by stable handle + packet_get Retrieve bounded evidence by file path or stable target search Deterministic ranked search with stable handles get_file Bounded project file reads inside the root get_symbol Resolve a search/explain handle diff --git a/src/cli/options.ts b/src/cli/options.ts index f1ac5ebb..5b80389a 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -380,7 +380,7 @@ const CLI_COMMAND_SCHEMAS = new Map([ { kind: "max", max: 2, - usage: "Usage: codegraph packet get [--root ] [--json | --pretty]", + usage: "Usage: codegraph packet get [--root ] [--json | --pretty]", }, ), ], diff --git a/src/cli/orient.ts b/src/cli/orient.ts index 60662384..5a01a5d9 100644 --- a/src/cli/orient.ts +++ b/src/cli/orient.ts @@ -4,6 +4,7 @@ import { type AgentOrientHealthMode, type AgentOrientResponse, } from "../agent/orient.js"; +import type { BuildOptions } from "../indexer/types.js"; import type { CliAgentCommandContext } from "./context.js"; export type OrientCommandContext = CliAgentCommandContext; @@ -26,15 +27,17 @@ function parseAgentOrientHealthMode(rawValue: string | undefined): AgentOrientHe export async function handleOrientCommand(context: OrientCommandContext): Promise { const healthMode = parseAgentOrientHealthMode(context.getOpt("--health")); + const writesJson = context.hasFlag("--json") || !context.hasFlag("--pretty"); + const buildOptions: BuildOptions = { ...(context.buildOptions ?? {}), logLevel: "silent" }; const response = await orientCodegraph({ root: context.root, includeRoots: context.positionals, budget: parseAgentOrientBudget(context.getOpt("--budget")), ...(healthMode !== undefined ? { health: healthMode } : {}), - ...(context.buildOptions ? { buildOptions: context.buildOptions } : {}), + ...(buildOptions ? { buildOptions } : {}), }); - if (context.hasFlag("--json") || !context.hasFlag("--pretty")) { + if (writesJson) { context.writeJSONLine(response); } else { context.writeStdoutLine(formatAgentOrientation(response)); @@ -42,17 +45,15 @@ export async function handleOrientCommand(context: OrientCommandContext): Promis } export function formatAgentOrientation(response: AgentOrientResponse): string { - const lines: string[] = ["Summary", ...response.summary.map((entry) => `- ${entry}`), "", "Tree"]; - if (response.tree.length) { - lines.push(...response.tree.map(formatTreeEntry)); - } else { - lines.push("- No files in scope."); - } + const lines: string[] = ["Summary", ...response.summary.map((entry) => `- ${entry}`)]; - if (response.modules.length) { - lines.push("", "Hotspots"); - for (const module of response.modules) { - lines.push(`- ${module.file} fan-in ${module.fanIn}, fan-out ${module.fanOut}, score ${module.score}`); + if (response.focus.length) { + lines.push("", "Start here"); + for (const focus of response.focus) { + lines.push(`- ${focus.file ?? focus.label}: ${focus.why}`); + for (const followUp of focus.followUps) { + lines.push(` - ${followUp}`); + } } } @@ -60,6 +61,20 @@ export function formatAgentOrientation(response: AgentOrientResponse): string { for (const next of response.recommendedNext) { lines.push(`- ${next.command}`); } + + lines.push("", "Tree"); + if (response.tree.length) { + lines.push(...response.tree.map(formatTreeEntry)); + } else { + lines.push("- No files in scope."); + } + + if (response.omittedCounts.treeEntries || response.omittedCounts.focusTargets) { + lines.push( + "", + `Omitted: ${response.omittedCounts.treeEntries} tree entries, ${response.omittedCounts.focusTargets} focus targets.`, + ); + } return lines.join("\n"); } diff --git a/src/cli/packet.ts b/src/cli/packet.ts index c8a136cb..919a847e 100644 --- a/src/cli/packet.ts +++ b/src/cli/packet.ts @@ -8,8 +8,8 @@ export type PacketCommandContext = CliAgentCommandContext; export async function handlePacketCommand(context: PacketCommandContext): Promise { const subcommand = context.positionals[0]; - const handle = context.positionals[1]; - if (subcommand !== "get" || handle === undefined) { + const target = context.positionals[1]; + if (subcommand !== "get" || target === undefined) { context.writeStderrLine(PACKET_HELP_TEXT.trimEnd()); context.exit(2); } @@ -17,7 +17,7 @@ export async function handlePacketCommand(context: PacketCommandContext): Promis try { const response = await getCodegraphPacket({ root: context.root, - handle, + target, ...(context.buildOptions ? { buildOptions: context.buildOptions } : {}), ...(context.getOpt("--max-symbols") !== undefined ? { maxSymbols: parsePositiveIntegerOption(context.getOpt("--max-symbols"), "--max-symbols", 50) } diff --git a/src/index.ts b/src/index.ts index 481713b2..44ff7216 100644 --- a/src/index.ts +++ b/src/index.ts @@ -225,20 +225,18 @@ export { export { createAgentSession } from "./agent/session.js"; export type { AgentProjectSnapshot, AgentSession, AgentSessionOptions } from "./agent/session.js"; -/** Agent first-turn orientation packets with stable follow-up handles. */ +/** Agent first-turn orientation packets with file-path follow-up targets. */ export { orientCodegraph } from "./agent/orient.js"; export type { - AgentModuleSummary, AgentOrientBudget, AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, AgentPacketCommand, - AgentPacketHandle, AgentTreeEntry, } from "./agent/orient.js"; -/** Agent packet retrieval by stable handle. */ +/** Agent packet retrieval by file path or stable target. */ export { getCodegraphPacket } from "./agent/packet.js"; export type { AgentPacketKind, AgentPacketPayload, AgentPacketRequest, AgentPacketResponse } from "./agent/packet.js"; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 7aa1f251..222ed7a3 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -111,7 +111,7 @@ export type CodegraphMcpHandlers = { budget?: AgentOrientBudget | undefined; }) => Promise; packet_get: (request: { - handle: string; + target: string; maxSymbols?: number | undefined; maxSnippets?: number | undefined; maxDuplicates?: number | undefined; @@ -283,7 +283,7 @@ function createCodegraphMcpHandlersForSession( packet_get: async (request) => await getCodegraphPacketWithSession(session, { root, - handle: request.handle, + target: request.target, ...(request.maxSymbols !== undefined ? { maxSymbols: request.maxSymbols } : {}), ...(request.maxSnippets !== undefined ? { maxSnippets: request.maxSnippets } : {}), ...(request.maxDuplicates !== undefined ? { maxDuplicates: request.maxDuplicates } : {}), @@ -742,7 +742,7 @@ const orientSchema = z.object({ }); const packetGetSchema = z.object({ - handle: z.string(), + target: z.string(), maxSymbols: z.number().int().positive().max(200).optional(), maxSnippets: z.number().int().positive().max(50).optional(), maxDuplicates: z.number().int().positive().max(20).optional(), diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 58b66f68..78c17ca8 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -56,15 +56,15 @@ export const MCP_TOOLS: Tool[] = [ }, { name: "packet_get", - description: "Retrieve a bounded evidence packet by stable handle.", + description: "Retrieve a bounded evidence packet by file path or stable target.", inputSchema: objectSchema( { - handle: stringProperty, + target: stringProperty, maxSymbols: { type: "integer", minimum: 1, maximum: 200 }, maxSnippets: { type: "integer", minimum: 1, maximum: 50 }, maxDuplicates: { type: "integer", minimum: 1, maximum: 20 }, }, - ["handle"], + ["target"], ), }, { diff --git a/tests/agent-orient.test.ts b/tests/agent-orient.test.ts index 7ddac227..a7d27b4e 100644 --- a/tests/agent-orient.test.ts +++ b/tests/agent-orient.test.ts @@ -19,17 +19,17 @@ describe("agent orient", () => { vi.restoreAllMocks(); }); - it("returns compact orientation with stable packet handles", async () => { + it("returns compact orientation with file focus targets", async () => { const root = await mkTmpDir("cg-agent-orient-"); await writeFile(root, "src/index.ts", "export { run } from './run';\n"); await writeFile(root, "src/run.ts", "export function run() { return 1; }\n"); const response = await orientCodegraph({ root, includeRoots: ["src"], budget: "small" }); - expect(response.schemaVersion).toBe(1); + expect(response.schemaVersion).toBe(2); expect(response.summary.length).toBeGreaterThan(0); expect(response.tree.some((entry) => entry.path === "src/index.ts")).toBe(true); - expect(response.handles.some((handle) => handle.handle.startsWith("file:"))).toBe(true); + expect(response.focus.some((focus) => focus.file === "src/index.ts")).toBe(true); expect(response.recommendedNext.length).toBeGreaterThan(0); }); @@ -58,12 +58,17 @@ describe("agent orient", () => { const response = await orientCodegraph({ root, includeRoots: ["."], budget: "small" }); expect(response.tree.some((entry) => entry.path === "src/index.ts")).toBe(true); - expect(response.handles.some((handle) => handle.file === "src/index.ts")).toBe(true); - expect(response.modules).toHaveLength(0); - expect(response.omittedCounts.hotspots).toBe(0); - expect(response.recommendedNext.some((next) => next.command === "codegraph hotspots . --limit 20 --json")).toBe( - true, - ); + expect(response.focus.some((focus) => focus.file === "src/index.ts")).toBe(true); + expect(response.omittedCounts.focusTargets).toBe(0); + expect(response.recommendedNext.some((next) => next.command === "codegraph hotspots . --limit 20")).toBe(true); + expect( + response.recommendedNext.some((next) => next.command === "codegraph impact --base HEAD --head WORKTREE --pretty"), + ).toBe(true); + expect( + response.recommendedNext.some( + (next) => next.command === "codegraph review --base HEAD --head WORKTREE --summary", + ), + ).toBe(true); }); it("normalizes absolute include roots against the project root", async () => { @@ -75,8 +80,25 @@ describe("agent orient", () => { expect(response.tree.some((entry) => entry.path === "src/index.ts")).toBe(true); expect(response.tree.some((entry) => entry.path === "docs/guide.md")).toBe(false); - expect(response.handles.some((handle) => handle.file === "src/index.ts")).toBe(true); - expect(response.handles.some((handle) => handle.file === "docs/guide.md")).toBe(false); + expect(response.focus.some((focus) => focus.file === "src/index.ts")).toBe(true); + expect(response.focus.some((focus) => focus.file === "docs/guide.md")).toBe(false); + }); + + it("prioritizes graph-central files over shallow root files", async () => { + const root = await mkTmpDir("cg-agent-orient-hotspot-first-"); + await writeFile(root, "package.json", '{"name":"sample"}\n'); + await writeFile(root, "src/core.ts", "export function core() { return 1; }\n"); + await writeFile(root, "src/alpha.ts", "import { core } from './core';\nexport const alpha = core();\n"); + await writeFile(root, "src/beta.ts", "import { core } from './core';\nexport const beta = core();\n"); + await writeFile(root, "src/gamma.ts", "import { core } from './core';\nexport const gamma = core();\n"); + + const response = await orientCodegraph({ root, includeRoots: ["."], budget: "small" }); + + expect(response.focus[0]?.file).toBe("src/core.ts"); + expect(response.focus[0]?.kind).toBe("hotspot"); + expect(response.focus[0]?.followUps.some((followUp) => followUp.includes("codegraph explain src/core.ts"))).toBe( + true, + ); }); it("uses small budget to skip deep health analysis", async () => { diff --git a/tests/agent-packet.test.ts b/tests/agent-packet.test.ts index a7afb5cc..d5dc1690 100644 --- a/tests/agent-packet.test.ts +++ b/tests/agent-packet.test.ts @@ -12,17 +12,17 @@ async function writeFile(root: string, relativePath: string, content: string): P } describe("agent packet", () => { - it("retrieves a file packet from an orientation handle", async () => { + it("retrieves a file packet from an orientation file target", async () => { const root = await mkTmpDir("cg-agent-packet-"); await writeFile(root, "src/run.ts", "export function run() { return 1; }\n"); const orient = await orientCodegraph({ root, includeRoots: ["src"], budget: "small" }); - const fileHandle = orient.handles.find((handle) => handle.kind === "file"); - if (!fileHandle) { - throw new Error("expected file handle"); + const target = orient.focus.find((focus) => focus.file); + if (!target?.file) { + throw new Error("expected file focus target"); } - const packet = await getCodegraphPacket({ root, handle: fileHandle.handle }); + const packet = await getCodegraphPacket({ root, target: target.file }); expect(packet.schemaVersion).toBe(1); expect(packet.kind).toBe("file"); @@ -49,7 +49,7 @@ export function normalizeInvoiceRows(rows: Array<{ amount: number; tax: number } await writeFile(root, "src/a.ts", source); await writeFile(root, "src/b.ts", source); - const packet = await getCodegraphPacket({ root, handle: "file:src%2Fa.ts", maxDuplicates: 1 }); + const packet = await getCodegraphPacket({ root, target: "src/a.ts", maxDuplicates: 1 }); if (packet.packet.schemaVersion !== 1 || !("duplicates" in packet.packet)) { throw new Error("expected explanation packet"); @@ -58,7 +58,7 @@ export function normalizeInvoiceRows(rows: Array<{ amount: number; tax: number } expect(packet.omittedCounts.duplicates).toBe(0); }); - it("retrieves review packets from orientation handles", async () => { + it("retrieves review packets from review targets", async () => { const root = await mkTmpDir("cg-agent-packet-review-"); runGit(root, ["init"]); runGit(root, ["symbolic-ref", "HEAD", "refs/heads/main"]); @@ -77,12 +77,10 @@ export function normalizeInvoiceRows(rows: Array<{ amount: number; tax: number } budget: "small", review: { base: "HEAD~1", head: "HEAD" }, }); - const reviewHandle = orient.handles.find((handle) => handle.kind === "review"); - if (!reviewHandle) { - throw new Error("expected review handle"); - } + expect(orient.focus[0]?.kind).toBe("review"); + expect(orient.focus[0]?.followUps[0]).toBe("codegraph review --base 'HEAD~1' --head HEAD --summary"); - const packet = await getCodegraphPacket({ root, handle: reviewHandle.handle }); + const packet = await getCodegraphPacket({ root, target: "review:base=HEAD~1;head=HEAD" }); expect(packet.schemaVersion).toBe(1); expect(packet.kind).toBe("review"); @@ -98,8 +96,8 @@ export function normalizeInvoiceRows(rows: Array<{ amount: number; tax: number } const root = await mkTmpDir("cg-agent-packet-malformed-review-"); await writeFile(root, "src/run.ts", "export function run() { return 1; }\n"); - await expect(getCodegraphPacket({ root, handle: "review:base=%;head=HEAD" })).rejects.toThrow( - "Invalid review packet handle", + await expect(getCodegraphPacket({ root, target: "review:base=%;head=HEAD" })).rejects.toThrow( + "Invalid review packet target", ); }); }); diff --git a/tests/cli-command-modules.test.ts b/tests/cli-command-modules.test.ts index f4f17397..a168c5f7 100644 --- a/tests/cli-command-modules.test.ts +++ b/tests/cli-command-modules.test.ts @@ -281,10 +281,10 @@ describe("CLI command modules", () => { expect(MCP_SERVE_HELP_TEXT).toContain("refresh_index"); }); - test("packet help does not imply CLI orient accepts review ranges", () => { - expect(PACKET_HELP_TEXT).toContain("CLI orient returns file handles"); - expect(PACKET_HELP_TEXT).toContain("library orientation calls that include a review range"); - expect(PACKET_HELP_TEXT).not.toContain("Review handles are returned by orient when a review range is requested"); + test("packet help documents file path targets", () => { + expect(PACKET_HELP_TEXT).toContain("project file paths"); + expect(PACKET_HELP_TEXT).toContain("search or explain output"); + expect(PACKET_HELP_TEXT).not.toContain("CLI orient returns file handles"); }); test("search command prints usage before running without a query", async () => { @@ -324,7 +324,7 @@ describe("CLI command modules", () => { ), ).rejects.toThrow("agent command exit 2"); - expect(stderr.join("\n")).toContain("Usage: codegraph packet get "); + expect(stderr.join("\n")).toContain("Usage: codegraph packet get "); }); test("goto command validates positional shape before indexing", async () => { @@ -376,7 +376,7 @@ describe("CLI command modules", () => { { args: ["packet", "--help"], heading: "codegraph packet", - usage: "Usage: codegraph packet get ", + usage: "Usage: codegraph packet get ", }, { args: ["explain", "--help"], diff --git a/tests/cli-regressions.test.ts b/tests/cli-regressions.test.ts index 087ada63..e5988e45 100644 --- a/tests/cli-regressions.test.ts +++ b/tests/cli-regressions.test.ts @@ -1403,13 +1403,13 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) const stdout = await runCliCommand(["orient", "src", "--root", root, "--budget", "small", "--json"]); const response = JSON.parse(stdout) as { schemaVersion: number; + focus: Array<{ file?: string }>; tree: Array<{ path: string }>; - handles: Array<{ handle: string }>; }; - expect(response.schemaVersion).toBe(1); + expect(response.schemaVersion).toBe(2); expect(response.tree.some((entry) => entry.path === "src/run.ts")).toBeTruthy(); - expect(response.handles.some((handle) => handle.handle.startsWith("file:"))).toBeTruthy(); + expect(response.focus.some((focus) => focus.file === "src/run.ts")).toBeTruthy(); }); it("orient treats a single positional as an include root", async () => { @@ -1422,14 +1422,14 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) const stdout = await runCliCommandDetailed(["orient", "src", "--budget", "small", "--json"], undefined, root); const response = JSON.parse(stdout.stdout) as { tree: Array<{ path: string }>; - handles: Array<{ file?: string }>; + focus: Array<{ file?: string }>; summary: string[]; }; expect(response.tree.some((entry) => entry.path === "src/run.ts")).toBeTruthy(); expect(response.tree.some((entry) => entry.path === "docs/guide.md")).toBeFalsy(); - expect(response.handles.some((handle) => handle.file === "src/run.ts")).toBeTruthy(); - expect(response.handles.some((handle) => handle.file === "docs/guide.md")).toBeFalsy(); + expect(response.focus.some((focus) => focus.file === "src/run.ts")).toBeTruthy(); + expect(response.focus.some((focus) => focus.file === "docs/guide.md")).toBeFalsy(); expect(response.summary).toContain("1 file(s) in scope."); }); @@ -1441,18 +1441,19 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) const stdout = await runCliCommand(["orient", "src", "--root", root, "--budget", "small", "--pretty"]); expect(stdout).toContain("Summary"); + expect(stdout).toContain("Start here"); expect(stdout).toContain("Tree"); expect(stdout).toContain("Recommended next"); expect(stdout).toContain("codegraph packet get"); - expect(stdout).toContain("file:"); + expect(stdout).toContain("src/run.ts"); }); - it("packet get returns a bounded packet for file handles", async () => { + it("packet get returns a bounded packet for file paths", async () => { const root = await fsp.mkdtemp(path.join(os.tmpdir(), "cg-cli-packet-")); await fsp.mkdir(path.join(root, "src"), { recursive: true }); await fsp.writeFile(path.join(root, "src", "run.ts"), "export function run() { return 1; }\n"); - const stdout = await runCliCommand(["packet", "get", "file:src%2Frun.ts", "--root", root, "--json"]); + const stdout = await runCliCommand(["packet", "get", "src/run.ts", "--root", root, "--json"]); const response = JSON.parse(stdout) as { schemaVersion: number; kind: string; @@ -1464,11 +1465,11 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) expect(response.packet.target.file).toBe("src/run.ts"); }); - it("packet get reports accepted prefixes for invalid handles", async () => { + it("packet get reports unresolved file targets", async () => { const root = await fsp.mkdtemp(path.join(os.tmpdir(), "cg-cli-packet-invalid-")); await expect(runCliCommandDetailed(["packet", "get", "bogus:thing", "--root", root])).rejects.toThrow( - /Expected one of: file:, symbol:, chunk:, sql:, graph:/, + "Packet target did not resolve: bogus:thing", ); }); diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts index fac23001..148762dc 100644 --- a/tests/mcp-server.test.ts +++ b/tests/mcp-server.test.ts @@ -287,10 +287,10 @@ describe("codegraph MCP handlers", () => { const handlers = createCodegraphMcpHandlers({ root }); const orient = await handlers.orient({ includeRoots: ["src"], budget: "small" }); - const fileHandle = orient.handles.find((handle) => handle.kind === "file"); - expect(fileHandle?.handle).toBeTruthy(); + const fileFocus = orient.focus.find((focus) => focus.file); + expect(fileFocus?.file).toBe("src/run.ts"); - const packet = await handlers.packet_get({ handle: fileHandle!.handle }); + const packet = await handlers.packet_get({ target: fileFocus!.file! }); expect(packet.schemaVersion).toBe(1); expect(packet.kind).toBe("file"); @@ -311,6 +311,7 @@ describe("codegraph MCP handlers", () => { expect(orientProperties.root).toBeUndefined(); expect(packetProperties.root).toBeUndefined(); expect(packetProperties.maxDuplicates).toBeTruthy(); + expect(packetProperties.target).toBeTruthy(); }); it("bounds refs by handle with the refs limit", async () => { diff --git a/tests/package-metadata.test.ts b/tests/package-metadata.test.ts index fcd41519..aa83fce1 100644 --- a/tests/package-metadata.test.ts +++ b/tests/package-metadata.test.ts @@ -656,7 +656,7 @@ void onImpactItemStreaming; ]); const sharedExamples = [ { - command: "codegraph orient --root . --budget small --json", + command: "codegraph orient --root . --budget small --pretty", files: ["README.md", "docs/cli.md", "docs/agent-workflows.md", "codegraph-skill/codegraph/SKILL.md"], }, { From b23550a6d49f02dc5cbc1715801a8fd29de005f2 Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 15:15:49 -0400 Subject: [PATCH 2/6] Harden packet target contracts --- src/agent.ts | 1 + src/agent/orient.ts | 32 +++++++++++++++++++++++-------- src/agent/packet.ts | 20 +++++++++++-------- src/cli/help.ts | 2 +- src/index.ts | 1 + tests/agent-orient.test.ts | 20 +++++++++++++++++++ tests/agent-packet.test.ts | 18 +++++++++++++++-- tests/cli-command-modules.test.ts | 2 +- tests/cli-regressions.test.ts | 12 +++++++++++- tests/mcp-server.test.ts | 2 +- 10 files changed, 88 insertions(+), 22 deletions(-) diff --git a/src/agent.ts b/src/agent.ts index 96468443..bce82b3d 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -6,6 +6,7 @@ export type { AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, + AgentOrientationFocus, AgentPacketCommand, AgentTreeEntry, } from "./agent/orient.js"; diff --git a/src/agent/orient.ts b/src/agent/orient.ts index d217e7f4..2b14f57d 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -6,7 +6,6 @@ import { getUnresolvedImports } from "../graphs/unresolved.js"; import type { BuildOptions } from "../indexer/types.js"; import type { Graph } from "../types.js"; import { normalizePath } from "../util/paths.js"; -import { formatAgentFileHandle } from "./handles.js"; import { createAgentSession, type AgentSession } from "./session.js"; import { quoteShellArg } from "./shell.js"; @@ -83,14 +82,14 @@ const ORIENT_BUDGETS: Record< { treeDepth: number; maxTreeEntries: number; - maxHandles: number; + maxFocusTargets: number; maxHotspots: number; includeHealth: boolean; } > = { - small: { treeDepth: 2, maxTreeEntries: 25, maxHandles: 5, maxHotspots: 5, includeHealth: false }, - medium: { treeDepth: 3, maxTreeEntries: 80, maxHandles: 10, maxHotspots: 10, includeHealth: true }, - large: { treeDepth: 4, maxTreeEntries: 160, maxHandles: 15, maxHotspots: 15, includeHealth: true }, + small: { treeDepth: 2, maxTreeEntries: 25, maxFocusTargets: 5, maxHotspots: 5, includeHealth: false }, + medium: { treeDepth: 3, maxTreeEntries: 80, maxFocusTargets: 10, maxHotspots: 10, includeHealth: true }, + large: { treeDepth: 4, maxTreeEntries: 160, maxFocusTargets: 15, maxHotspots: 15, includeHealth: true }, }; export async function orientCodegraph(request: AgentOrientRequest): Promise { @@ -132,7 +131,7 @@ export async function orientCodegraphWithSession( const packets = buildFilePackets( scopedFiles, modules.map((module) => module.file), - limits.maxHandles, + limits.maxFocusTargets, ); const focus = buildFocusTargets(modules, packets, request.review); const healthMode = request.health ?? (limits.includeHealth ? "summary" : "skip"); @@ -349,7 +348,7 @@ function buildFocusTargets( kind: "hotspot" as const, file: module.file, why: `graph-central module: fan-in ${module.fanIn}, fan-out ${module.fanOut}, score ${module.score}`, - followUps: [formatPacketCommand(packet.file), `codegraph explain ${quoteShellArg(module.file)}`], + followUps: [formatPacketCommand(packet.file), `codegraph explain ${formatFileTargetCommandArg(module.file)}`], }); continue; } @@ -396,7 +395,24 @@ function buildRecommendedNext(includeRoots: string[], focus: AgentOrientationFoc } function formatPacketCommand(file: string): string { - return `codegraph packet get ${quoteShellArg(file)} --pretty`; + return `codegraph packet get ${formatFileTargetCommandArg(file)} --pretty`; +} + +function formatFileTargetCommandArg(file: string): string { + const target = fileNeedsRelativePrefix(file) ? `./${file}` : file; + return quoteShellArg(target); +} + +function fileNeedsRelativePrefix(file: string): boolean { + return ( + file.startsWith("-") || + file.startsWith("file:") || + file.startsWith("symbol:") || + file.startsWith("chunk:") || + file.startsWith("sql:") || + file.startsWith("graph:") || + file.startsWith("review:") + ); } function labelForFollowUp(focus: AgentOrientationFocus, followUp: string): string { diff --git a/src/agent/packet.ts b/src/agent/packet.ts index ee53cc39..89c8f299 100644 --- a/src/agent/packet.ts +++ b/src/agent/packet.ts @@ -18,7 +18,7 @@ export type AgentPacketRequest = { export type AgentPacketPayload = AgentExplanation | ReviewReport; export type AgentPacketResponse = { - schemaVersion: 1; + schemaVersion: 2; root: string; target: string; kind: AgentPacketKind; @@ -56,7 +56,7 @@ export async function getCodegraphPacketWithSession( async function buildExplainPacket( session: AgentSession, request: AgentPacketRequest, - kind: AgentPacketKind, + kind: AgentPacketKind | null, ): Promise { const explainRequest: AgentExplainTarget = { root: request.root, @@ -78,10 +78,10 @@ async function buildExplainPacket( } return { - schemaVersion: 1, + schemaVersion: 2, root: explanation.root, target: request.target, - kind, + kind: kind ?? packetKindForResolvedTarget(explanation.target.kind), packet: explanation, limits: explanation.limits, omittedCounts: explanation.omittedCounts, @@ -89,9 +89,13 @@ async function buildExplainPacket( }; } -function inferPacketKind(target: string): AgentPacketKind { - const kind = kindForHandle(target); - if (kind) return kind; +function inferPacketKind(target: string): AgentPacketKind | null { + return kindForHandle(target); +} + +function packetKindForResolvedTarget(kind: AgentExplanation["target"]["kind"]): AgentPacketKind { + if (kind === "sql_object") return "sql_object"; + if (kind === "symbol") return "symbol"; return "file"; } @@ -106,7 +110,7 @@ async function buildReviewPacket(request: AgentPacketRequest): Promise [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] Targets: - Accepts project file paths plus symbol:, chunk:, sql:, graph:, and review: handles from search or explain output. + Accepts project file paths plus symbol:, chunk:, sql:, and graph: handles from search or explain output; review packets use quoted 'review:base=;head=' targets. Index options: Supports shared --cache, --cache-strict, --cache-verify, --threads, --native, --workers, --include-glob, --ignore-glob, and --no-gitignore options. diff --git a/src/index.ts b/src/index.ts index 44ff7216..68754a45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,6 +232,7 @@ export type { AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, + AgentOrientationFocus, AgentPacketCommand, AgentTreeEntry, } from "./agent/orient.js"; diff --git a/tests/agent-orient.test.ts b/tests/agent-orient.test.ts index a7d27b4e..0c585706 100644 --- a/tests/agent-orient.test.ts +++ b/tests/agent-orient.test.ts @@ -33,6 +33,26 @@ describe("agent orient", () => { expect(response.recommendedNext.length).toBeGreaterThan(0); }); + it("quotes dash-prefixed file targets in follow-up commands", async () => { + const root = await mkTmpDir("cg-agent-orient-dash-file-"); + await writeFile(root, "-entry.ts", "export const value = 1;\n"); + + const response = await orientCodegraph({ root, includeRoots: ["."], budget: "small" }); + + expect(response.focus[0]?.file).toBe("-entry.ts"); + expect(response.focus[0]?.followUps[0]).toBe("codegraph packet get ./-entry.ts --pretty"); + }); + + it("disambiguates handle-like file targets in follow-up commands", async () => { + const root = await mkTmpDir("cg-agent-orient-handle-like-file-"); + await writeFile(root, "file:entry.ts", "export const value = 1;\n"); + + const response = await orientCodegraph({ root, includeRoots: ["."], budget: "small" }); + + expect(response.focus[0]?.file).toBe("file:entry.ts"); + expect(response.focus[0]?.followUps[0]).toBe("codegraph packet get ./file:entry.ts --pretty"); + }); + it("does not build the detailed symbol graph for orientation", async () => { const root = await mkTmpDir("cg-agent-orient-skip-symbol-graph-"); await writeFile(root, "src/index.ts", "export { run } from './run';\n"); diff --git a/tests/agent-packet.test.ts b/tests/agent-packet.test.ts index d5dc1690..760f4cdc 100644 --- a/tests/agent-packet.test.ts +++ b/tests/agent-packet.test.ts @@ -24,11 +24,25 @@ describe("agent packet", () => { const packet = await getCodegraphPacket({ root, target: target.file }); - expect(packet.schemaVersion).toBe(1); + expect(packet.schemaVersion).toBe(2); expect(packet.kind).toBe("file"); expect(packet.followUps.length).toBeGreaterThan(0); }); + it("reports bare symbol packet targets as symbols", async () => { + const root = await mkTmpDir("cg-agent-packet-symbol-"); + await writeFile(root, "src/auth.ts", "export function validateUser() { return true; }\n"); + + const packet = await getCodegraphPacket({ root, target: "validateUser" }); + + expect(packet.schemaVersion).toBe(2); + expect(packet.kind).toBe("symbol"); + if (packet.packet.schemaVersion !== 1) { + throw new Error("expected explanation packet"); + } + expect(packet.packet.target.kind).toBe("symbol"); + }); + it("retrieves duplicate context in file packets", async () => { const root = await mkTmpDir("cg-agent-packet-dups-"); const source = ` @@ -82,7 +96,7 @@ export function normalizeInvoiceRows(rows: Array<{ amount: number; tax: number } const packet = await getCodegraphPacket({ root, target: "review:base=HEAD~1;head=HEAD" }); - expect(packet.schemaVersion).toBe(1); + expect(packet.schemaVersion).toBe(2); expect(packet.kind).toBe("review"); expect(packet.followUps.some((command) => command.includes("codegraph review"))).toBeTruthy(); if (packet.packet.schemaVersion !== 2) { diff --git a/tests/cli-command-modules.test.ts b/tests/cli-command-modules.test.ts index a168c5f7..f29a1893 100644 --- a/tests/cli-command-modules.test.ts +++ b/tests/cli-command-modules.test.ts @@ -312,7 +312,7 @@ describe("CLI command modules", () => { ).rejects.toThrow("Invalid --mode value"); }); - test("packet command validates subcommand and handle before lookup", async () => { + test("packet command validates subcommand and target before lookup", async () => { const stderr: string[] = []; await expect( diff --git a/tests/cli-regressions.test.ts b/tests/cli-regressions.test.ts index e5988e45..7ebbde24 100644 --- a/tests/cli-regressions.test.ts +++ b/tests/cli-regressions.test.ts @@ -1460,9 +1460,19 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) packet: { target: { file?: string } }; }; - expect(response.schemaVersion).toBe(1); + expect(response.schemaVersion).toBe(2); expect(response.kind).toBe("file"); expect(response.packet.target.file).toBe("src/run.ts"); + + const handleStdout = await runCliCommand(["packet", "get", "file:src%2Frun.ts", "--root", root, "--json"]); + const handleResponse = JSON.parse(handleStdout) as { + schemaVersion: number; + kind: string; + packet: { target: { file?: string } }; + }; + expect(handleResponse.schemaVersion).toBe(2); + expect(handleResponse.kind).toBe("file"); + expect(handleResponse.packet.target.file).toBe("src/run.ts"); }); it("packet get reports unresolved file targets", async () => { diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts index 148762dc..8cf7db5d 100644 --- a/tests/mcp-server.test.ts +++ b/tests/mcp-server.test.ts @@ -292,7 +292,7 @@ describe("codegraph MCP handlers", () => { const packet = await handlers.packet_get({ target: fileFocus!.file! }); - expect(packet.schemaVersion).toBe(1); + expect(packet.schemaVersion).toBe(2); expect(packet.kind).toBe("file"); expect(JSON.stringify(packet.packet)).toContain("src/run.ts"); }); From 2bdb44813960f38cd44ba5b7e938a3f4da14278f Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 18:45:08 -0400 Subject: [PATCH 3/6] Address orient output review feedback --- src/agent/orient.ts | 32 +++++++++++++++++++++----------- src/cli/help.ts | 2 +- src/cli/orient.ts | 12 ++++++++---- tests/agent-orient.test.ts | 18 ++++++++++++++++++ tests/cli-regressions.test.ts | 1 + 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/agent/orient.ts b/src/agent/orient.ts index 2b14f57d..5224d7e5 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -5,6 +5,7 @@ import { getHotspots } from "../graphs/hotspots.js"; import { getUnresolvedImports } from "../graphs/unresolved.js"; import type { BuildOptions } from "../indexer/types.js"; import type { Graph } from "../types.js"; +import { isGitRepo } from "../util/git.js"; import { normalizePath } from "../util/paths.js"; import { createAgentSession, type AgentSession } from "./session.js"; import { quoteShellArg } from "./shell.js"; @@ -135,8 +136,11 @@ export async function orientCodegraphWithSession( ); const focus = buildFocusTargets(modules, packets, request.review); const healthMode = request.health ?? (limits.includeHealth ? "summary" : "skip"); - const health = await buildHealth(root, snapshot.index, scopedAbsoluteFiles, scopedFileGraph, healthMode); - const recommendedNext = buildRecommendedNext(includeRoots, focus); + const [health, hasGitRepo] = await Promise.all([ + buildHealth(root, snapshot.index, scopedAbsoluteFiles, scopedFileGraph, healthMode), + isGitRepo(root), + ]); + const recommendedNext = buildRecommendedNext(includeRoots, focus, hasGitRepo); const healthSummary = formatHealthSummary(health); return { @@ -363,7 +367,11 @@ function buildFocusTargets( return focus; } -function buildRecommendedNext(includeRoots: string[], focus: AgentOrientationFocus[]): AgentPacketCommand[] { +function buildRecommendedNext( + includeRoots: string[], + focus: AgentOrientationFocus[], + hasGitRepo: boolean, +): AgentPacketCommand[] { const commands: AgentPacketCommand[] = []; const firstFocus = focus[0]; if (firstFocus) { @@ -379,14 +387,16 @@ function buildRecommendedNext(includeRoots: string[], focus: AgentOrientationFoc label: "Rank scoped hotspots", command: `codegraph hotspots ${quoteShellArg(firstRoot)} --limit 20`, }); - commands.push({ - label: "Map current worktree impact", - command: "codegraph impact --base HEAD --head WORKTREE --pretty", - }); - commands.push({ - label: "Review current worktree", - command: "codegraph review --base HEAD --head WORKTREE --summary", - }); + if (hasGitRepo) { + commands.push({ + label: "Map current worktree impact", + command: "codegraph impact --base HEAD --head WORKTREE --pretty", + }); + commands.push({ + label: "Review current worktree", + command: "codegraph review --base HEAD --head WORKTREE --summary", + }); + } commands.push({ label: "Search for a task-specific anchor", command: "codegraph search --json", diff --git a/src/cli/help.ts b/src/cli/help.ts index 956a25bb..815d9a40 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -166,7 +166,7 @@ export const PACKET_HELP_TEXT = `codegraph packet - Retrieve bounded evidence pa Usage: codegraph packet get [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] Targets: - Accepts project file paths plus symbol:, chunk:, sql:, and graph: handles from search or explain output; review packets use quoted 'review:base=;head=' targets. + Accepts project file paths plus file:, symbol:, chunk:, sql:, and graph: handles from search or explain output; review packets use quoted 'review:base=;head=' targets. Index options: Supports shared --cache, --cache-strict, --cache-verify, --threads, --native, --workers, --include-glob, --ignore-glob, and --no-gitignore options. diff --git a/src/cli/orient.ts b/src/cli/orient.ts index 5a01a5d9..356f0a8b 100644 --- a/src/cli/orient.ts +++ b/src/cli/orient.ts @@ -34,7 +34,7 @@ export async function handleOrientCommand(context: OrientCommandContext): Promis includeRoots: context.positionals, budget: parseAgentOrientBudget(context.getOpt("--budget")), ...(healthMode !== undefined ? { health: healthMode } : {}), - ...(buildOptions ? { buildOptions } : {}), + buildOptions, }); if (writesJson) { @@ -57,9 +57,13 @@ export function formatAgentOrientation(response: AgentOrientResponse): string { } } - lines.push("", "Recommended next"); - for (const next of response.recommendedNext) { - lines.push(`- ${next.command}`); + const focusFollowUps = new Set(response.focus.flatMap((focus) => focus.followUps)); + const remainingRecommendations = response.recommendedNext.filter((next) => !focusFollowUps.has(next.command)); + if (remainingRecommendations.length) { + lines.push("", "Recommended next"); + for (const next of remainingRecommendations) { + lines.push(`- ${next.command}`); + } } lines.push("", "Tree"); diff --git a/tests/agent-orient.test.ts b/tests/agent-orient.test.ts index 0c585706..b1888449 100644 --- a/tests/agent-orient.test.ts +++ b/tests/agent-orient.test.ts @@ -6,6 +6,7 @@ import { orientCodegraph, orientCodegraphWithSession } from "../src/agent/orient import * as duplicates from "../src/duplicates.js"; import * as symbolGraphBuild from "../src/graphs/symbol-graph-detailed.js"; import { countingSession } from "./helpers/agent.js"; +import { runGit } from "./helpers/git.js"; import { mkTmpDir } from "./helpers/filesystem.js"; async function writeFile(root: string, relativePath: string, content: string): Promise { @@ -81,6 +82,23 @@ describe("agent orient", () => { expect(response.focus.some((focus) => focus.file === "src/index.ts")).toBe(true); expect(response.omittedCounts.focusTargets).toBe(0); expect(response.recommendedNext.some((next) => next.command === "codegraph hotspots . --limit 20")).toBe(true); + expect( + response.recommendedNext.some((next) => next.command === "codegraph impact --base HEAD --head WORKTREE --pretty"), + ).toBe(false); + expect( + response.recommendedNext.some( + (next) => next.command === "codegraph review --base HEAD --head WORKTREE --summary", + ), + ).toBe(false); + }); + + it("recommends worktree review commands inside git repos", async () => { + const root = await mkTmpDir("cg-agent-orient-git-"); + runGit(root, ["init"]); + await writeFile(root, "src/index.ts", "export const value = 1;\n"); + + const response = await orientCodegraph({ root, includeRoots: ["."], budget: "small" }); + expect( response.recommendedNext.some((next) => next.command === "codegraph impact --base HEAD --head WORKTREE --pretty"), ).toBe(true); diff --git a/tests/cli-regressions.test.ts b/tests/cli-regressions.test.ts index 7ebbde24..eff64906 100644 --- a/tests/cli-regressions.test.ts +++ b/tests/cli-regressions.test.ts @@ -1446,6 +1446,7 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) expect(stdout).toContain("Recommended next"); expect(stdout).toContain("codegraph packet get"); expect(stdout).toContain("src/run.ts"); + expect(stdout.split("codegraph packet get").length - 1).toBe(1); }); it("packet get returns a bounded packet for file paths", async () => { From 684cec44344c74ab5c89029d776888aef27896d3 Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 20:39:23 -0400 Subject: [PATCH 4/6] Address orientation focus budget feedback --- src/agent.ts | 2 ++ src/agent/orient.ts | 25 ++++++++++++++++++++++--- src/index.ts | 2 ++ tests/agent-orient.test.ts | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/agent.ts b/src/agent.ts index bce82b3d..65b31881 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -2,12 +2,14 @@ export { createAgentSession } from "./agent/session.js"; export type { AgentProjectSnapshot, AgentSession, AgentSessionOptions } from "./agent/session.js"; export { orientCodegraph } from "./agent/orient.js"; export type { + AgentModuleSummary, AgentOrientBudget, AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, AgentOrientationFocus, AgentPacketCommand, + AgentPacketHandle, AgentTreeEntry, } from "./agent/orient.js"; export { getCodegraphPacket } from "./agent/packet.js"; diff --git a/src/agent/orient.ts b/src/agent/orient.ts index 5224d7e5..cdfc68be 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -31,7 +31,24 @@ export type AgentTreeEntry = { depth: number; }; -type AgentModuleSummary = { +/** @deprecated Orient schema v2 no longer returns module summaries directly; use AgentOrientationFocus instead. */ +export type AgentModuleSummary = { + file: string; + fanIn: number; + fanOut: number; + score: number; + handle: string; +}; + +/** @deprecated Orient schema v2 no longer returns packet handles; use file-path focus targets instead. */ +export type AgentPacketHandle = { + kind: "file" | "review"; + handle: string; + label: string; + file?: string; +}; + +type AgentFocusModuleSummary = { file: string; fanIn: number; fanOut: number; @@ -129,10 +146,12 @@ export async function orientCodegraphWithSession( fanOut: hotspot.fanOut, score: hotspot.score, })); + const reviewFocusSlots = request.review ? 1 : 0; + const maxFileFocusTargets = Math.max(0, limits.maxFocusTargets - reviewFocusSlots); const packets = buildFilePackets( scopedFiles, modules.map((module) => module.file), - limits.maxFocusTargets, + maxFileFocusTargets, ); const focus = buildFocusTargets(modules, packets, request.review); const healthMode = request.health ?? (limits.includeHealth ? "summary" : "skip"); @@ -335,7 +354,7 @@ function buildFilePackets(scopedFiles: string[], priorityFiles: string[], maxPac } function buildFocusTargets( - modules: AgentModuleSummary[], + modules: AgentFocusModuleSummary[], packets: AgentPacketRef[], review: AgentOrientRequest["review"] | undefined, ): AgentOrientationFocus[] { diff --git a/src/index.ts b/src/index.ts index 68754a45..61ecc9af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -228,12 +228,14 @@ export type { AgentProjectSnapshot, AgentSession, AgentSessionOptions } from "./ /** Agent first-turn orientation packets with file-path follow-up targets. */ export { orientCodegraph } from "./agent/orient.js"; export type { + AgentModuleSummary, AgentOrientBudget, AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, AgentOrientationFocus, AgentPacketCommand, + AgentPacketHandle, AgentTreeEntry, } from "./agent/orient.js"; diff --git a/tests/agent-orient.test.ts b/tests/agent-orient.test.ts index b1888449..27ef7e75 100644 --- a/tests/agent-orient.test.ts +++ b/tests/agent-orient.test.ts @@ -72,6 +72,24 @@ describe("agent orient", () => { expect(counted.loads()).toBe(1); }); + it("keeps review orientation within the focus target budget", async () => { + const root = await mkTmpDir("cg-agent-orient-review-budget-"); + for (let index = 0; index < 8; index += 1) { + await writeFile(root, `src/file-${index}.ts`, `export const value${index} = ${index};\n`); + } + + const response = await orientCodegraph({ + root, + includeRoots: ["src"], + budget: "small", + review: { base: "HEAD~1", head: "HEAD" }, + }); + + expect(response.focus).toHaveLength(5); + expect(response.focus[0]?.kind).toBe("review"); + expect(response.focus.filter((focus) => focus.file).length).toBe(4); + }); + it("treats dot include root as unscoped orientation", async () => { const root = await mkTmpDir("cg-agent-orient-dot-"); await writeFile(root, "src/index.ts", "export const value = 1;\n"); From eeb5dfef833a15494166ceeb76342539fc017df5 Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 20:58:28 -0400 Subject: [PATCH 5/6] Clarify packet target guidance --- codegraph-skill/codegraph/SKILL.md | 4 ++-- docs/cli.md | 2 +- docs/library-api.md | 2 +- docs/mcp.md | 2 +- src/cli/help.ts | 4 ++-- src/cli/options.ts | 2 +- tests/cli-command-modules.test.ts | 10 +++++----- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/codegraph-skill/codegraph/SKILL.md b/codegraph-skill/codegraph/SKILL.md index d96e3663..a486394b 100644 --- a/codegraph-skill/codegraph/SKILL.md +++ b/codegraph-skill/codegraph/SKILL.md @@ -30,9 +30,9 @@ For PR, worktree, or sweeping review tasks, start with `codegraph review --base Then choose the smallest useful follow-up: -- packet: `codegraph packet get --pretty` +- packet: `codegraph packet get --pretty` - search: `codegraph search "auth user" --json` -- explain: `codegraph explain ` +- explain: `codegraph explain ` - architecture: `codegraph inspect ./src --limit 20` - dependencies: `codegraph deps ` or `codegraph rdeps ` - path: `codegraph path ` diff --git a/docs/cli.md b/docs/cli.md index 09d910fc..986208fa 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -230,7 +230,7 @@ Short JSON shape: - Use `orient --pretty` as the compact first-turn reading surface for people or models; it prints the ranked `focus` targets and their follow-up commands before the scope sketch. - Use `orient --json` when follow-up tools need exact focus reasons, limits, and omitted counts. Orient suppresses index rebuild warnings so stdout stays parseable. - Small orientation budgets default to `--health skip`. Medium and large default to `--health summary`, which counts cycles and unresolved imports while omitting duplicate health; use `--health full` when exhaustive duplicate counts matter. -- Use `packet get` with file paths, symbol handles, chunk handles, SQL handles, graph handles, or review handles to retrieve bounded evidence plus follow-up commands. +- Use `packet get` with file paths, symbol names, SQL object names, file/symbol/chunk/SQL/graph handles, or review handles to retrieve bounded evidence plus follow-up commands. - Agent commands reuse the incremental index path and default to disk cache. Use shared index flags such as `--cache`, `--cache-strict`, `--cache-verify`, `--threads`, `--native`, `--workers`, `--include-glob`, `--ignore-glob`, and `--no-gitignore` when the packet should match a specific scan mode. `search` is deterministic and vectorless. `explain` resolves file paths, symbol names, SQL object names, and search handles into bounded packets with symbols, graph context, references, snippets, duplicate context, SQL facts, review tasks, candidate tests, limits, omissions, and follow-ups. Use `--max-duplicates` to tune duplicate context in `explain` and `packet get`; duplicate context also uses an internal pair budget and reports skipped duplicate work through omission counts. diff --git a/docs/library-api.md b/docs/library-api.md index b6e19d16..c90cb2fe 100644 --- a/docs/library-api.md +++ b/docs/library-api.md @@ -80,7 +80,7 @@ compatibility exports. ## Agent packets -`orientCodegraph()` returns compact first-turn context for an agent, and `getCodegraphPacket()` retrieves bounded evidence by file path or stable target: +`orientCodegraph()` returns compact first-turn context for an agent, and `getCodegraphPacket()` retrieves bounded evidence by file path, symbol name, SQL object name, or stable target: ```ts import { getCodegraphPacket, orientCodegraph } from "@lzehrung/codegraph"; diff --git a/docs/mcp.md b/docs/mcp.md index cf2f0411..800072ed 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -36,7 +36,7 @@ Use stdio for a client-owned subprocess. Use HTTP for one long-running Codegraph The server exposes the same bounded primitives as the CLI and library session layer: - `orient`: compact first-turn repo context. -- `packet_get`: bounded evidence packet by file path or stable target. +- `packet_get`: bounded evidence packet by file path, symbol name, SQL object name, or stable target. - `search`: deterministic ranked search across paths, symbols, chunks, SQL objects, and graph context. - `get_file`: bounded project file read. - `get_symbol`: resolve a stable search or explain handle. diff --git a/src/cli/help.ts b/src/cli/help.ts index 815d9a40..c0d9c149 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -163,10 +163,10 @@ Index options: export const PACKET_HELP_TEXT = `codegraph packet - Retrieve bounded evidence packets by file path or stable target -Usage: codegraph packet get [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] +Usage: codegraph packet get [--root ] [--json | --pretty] [--max-symbols ] [--max-snippets ] [--max-duplicates ] Targets: - Accepts project file paths plus file:, symbol:, chunk:, sql:, and graph: handles from search or explain output; review packets use quoted 'review:base=;head=' targets. + Accepts file paths, symbol names, SQL object names, file:/symbol:/chunk:/sql:/graph: handles from search or explain output, and quoted review packet targets like 'review:base=;head='. Index options: Supports shared --cache, --cache-strict, --cache-verify, --threads, --native, --workers, --include-glob, --ignore-glob, and --no-gitignore options. diff --git a/src/cli/options.ts b/src/cli/options.ts index 5b80389a..71ab6013 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -380,7 +380,7 @@ const CLI_COMMAND_SCHEMAS = new Map([ { kind: "max", max: 2, - usage: "Usage: codegraph packet get [--root ] [--json | --pretty]", + usage: "Usage: codegraph packet get [--root ] [--json | --pretty]", }, ), ], diff --git a/tests/cli-command-modules.test.ts b/tests/cli-command-modules.test.ts index f29a1893..160313a1 100644 --- a/tests/cli-command-modules.test.ts +++ b/tests/cli-command-modules.test.ts @@ -281,9 +281,9 @@ describe("CLI command modules", () => { expect(MCP_SERVE_HELP_TEXT).toContain("refresh_index"); }); - test("packet help documents file path targets", () => { - expect(PACKET_HELP_TEXT).toContain("project file paths"); - expect(PACKET_HELP_TEXT).toContain("search or explain output"); + test("packet help documents accepted target shapes", () => { + expect(PACKET_HELP_TEXT).toContain("file paths, symbol names, SQL object names"); + expect(PACKET_HELP_TEXT).toContain("file:/symbol:/chunk:/sql:/graph: handles"); expect(PACKET_HELP_TEXT).not.toContain("CLI orient returns file handles"); }); @@ -324,7 +324,7 @@ describe("CLI command modules", () => { ), ).rejects.toThrow("agent command exit 2"); - expect(stderr.join("\n")).toContain("Usage: codegraph packet get "); + expect(stderr.join("\n")).toContain("Usage: codegraph packet get "); }); test("goto command validates positional shape before indexing", async () => { @@ -376,7 +376,7 @@ describe("CLI command modules", () => { { args: ["packet", "--help"], heading: "codegraph packet", - usage: "Usage: codegraph packet get ", + usage: "Usage: codegraph packet get ", }, { args: ["explain", "--help"], From b9c3268377a050b8cdce4c8eeecca018e3c1f825 Mon Sep 17 00:00:00 2001 From: Luke Zehrung Date: Thu, 18 Jun 2026 23:21:32 -0400 Subject: [PATCH 6/6] Refine packet target metadata --- src/agent/orient.ts | 2 +- src/mcp/tools.ts | 2 +- tests/agent-orient.test.ts | 2 ++ tests/mcp-server.test.ts | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/agent/orient.ts b/src/agent/orient.ts index cdfc68be..9de0867d 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -249,7 +249,7 @@ function formatHealthSummary(health: { function buildReviewFocus(base: string, head: string): AgentOrientationFocus { return { kind: "review", - label: `Review ${base}..${head}`, + label: `${base}..${head}`, why: "review range requested by the caller", followUps: [ `codegraph review --base ${quoteShellArg(base)} --head ${quoteShellArg(head)} --summary`, diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 78c17ca8..9a05d28e 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -56,7 +56,7 @@ export const MCP_TOOLS: Tool[] = [ }, { name: "packet_get", - description: "Retrieve a bounded evidence packet by file path or stable target.", + description: "Retrieve a bounded evidence packet by file path, symbol name, SQL object name, or stable target.", inputSchema: objectSchema( { target: stringProperty, diff --git a/tests/agent-orient.test.ts b/tests/agent-orient.test.ts index 27ef7e75..80273eed 100644 --- a/tests/agent-orient.test.ts +++ b/tests/agent-orient.test.ts @@ -88,6 +88,8 @@ describe("agent orient", () => { expect(response.focus).toHaveLength(5); expect(response.focus[0]?.kind).toBe("review"); expect(response.focus.filter((focus) => focus.file).length).toBe(4); + expect(response.recommendedNext[0]?.label).toBe("Review HEAD~1..HEAD"); + expect(response.recommendedNext[1]?.label).toBe("Map impact for HEAD~1..HEAD"); }); it("treats dot include root as unscoped orientation", async () => { diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts index 8cf7db5d..fa1c8484 100644 --- a/tests/mcp-server.test.ts +++ b/tests/mcp-server.test.ts @@ -302,6 +302,8 @@ describe("codegraph MCP handlers", () => { const packetTool = listCodegraphMcpTools().find((tool) => tool.name === "packet_get"); expect(orientTool).toBeTruthy(); expect(packetTool).toBeTruthy(); + expect(packetTool?.description).toContain("symbol name"); + expect(packetTool?.description).toContain("SQL object name"); const orientSchema = readObject(orientTool!.inputSchema); const packetSchema = readObject(packetTool!.inputSchema);