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..a486394b 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..986208fa 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 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 16e648df..c90cb2fe 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, symbol name, SQL object name, 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..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 stable handle. +- `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/agent.ts b/src/agent.ts index 0a170637..65b31881 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -7,6 +7,7 @@ export type { AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, + AgentOrientationFocus, AgentPacketCommand, AgentPacketHandle, AgentTreeEntry, diff --git a/src/agent/orient.ts b/src/agent/orient.ts index 2df8de28..9de0867d 100644 --- a/src/agent/orient.ts +++ b/src/agent/orient.ts @@ -5,8 +5,8 @@ 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 { formatAgentFileHandle } from "./handles.js"; import { createAgentSession, type AgentSession } from "./session.js"; import { quoteShellArg } from "./shell.js"; @@ -31,6 +31,7 @@ export type AgentTreeEntry = { depth: number; }; +/** @deprecated Orient schema v2 no longer returns module summaries directly; use AgentOrientationFocus instead. */ export type AgentModuleSummary = { file: string; fanIn: number; @@ -39,6 +40,7 @@ export type AgentModuleSummary = { 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; @@ -46,29 +48,49 @@ export type AgentPacketHandle = { file?: string; }; +type AgentFocusModuleSummary = { + file: string; + fanIn: number; + fanOut: number; + score: number; +}; + +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 = { label: string; command: string; }; 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; }; }; @@ -78,14 +100,14 @@ const ORIENT_BUDGETS: Record< { treeDepth: number; maxTreeEntries: number; - maxHandles: number; + maxFocusTargets: number; maxHotspots: number; 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, 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 { @@ -123,43 +145,43 @@ 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 reviewFocusSlots = request.review ? 1 : 0; + const maxFileFocusTargets = Math.max(0, limits.maxFocusTargets - reviewFocusSlots); + const packets = buildFilePackets( + scopedFiles, + modules.map((module) => module.file), + maxFileFocusTargets, + ); + 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 [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 { - 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 +246,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}`, + label: `${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 +331,124 @@ function parentPath(file: string): string { return file.slice(0, separator); } +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: AgentFocusModuleSummary[], + packets: AgentPacketRef[], + review: AgentOrientRequest["review"] | undefined, +): AgentOrientationFocus[] { + const focus: AgentOrientationFocus[] = []; + if (review) { + focus.push(buildReviewFocus(review.base, review.head)); + } + 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 ${formatFileTargetCommandArg(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( - scopedFiles: string[], includeRoots: string[], - handles: AgentPacketHandle[], + focus: AgentOrientationFocus[], + hasGitRepo: boolean, ): AgentPacketCommand[] { const commands: AgentPacketCommand[] = []; - const firstHandle = handles[0]; - if (firstHandle) { - const label = firstHandle.file ?? firstHandle.label; + 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`, + }); + if (hasGitRepo) { commands.push({ - label: `Get packet for ${label}`, - command: `codegraph packet get ${quoteShellArg(firstHandle.handle)} --json`, + label: "Map current worktree impact", + command: "codegraph impact --base HEAD --head WORKTREE --pretty", }); - } - if (scopedFiles.length) { - const firstRoot = includeRoots[0] ?? "."; commands.push({ - label: "Inspect hotspots", - command: `codegraph hotspots ${quoteShellArg(firstRoot)} --limit 20 --json`, + label: "Review current worktree", + command: "codegraph review --base HEAD --head WORKTREE --summary", }); } commands.push({ - label: "Search for an anchor", + label: "Search for a task-specific anchor", command: "codegraph search --json", }); return commands; } + +function formatPacketCommand(file: string): string { + 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 { + 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..89c8f299 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; @@ -18,9 +18,9 @@ export type AgentPacketRequest = { export type AgentPacketPayload = AgentExplanation | ReviewReport; export type AgentPacketResponse = { - schemaVersion: 1; + schemaVersion: 2; 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); } @@ -58,11 +56,11 @@ export async function getCodegraphPacketWithSession( async function buildExplainPacket( session: AgentSession, request: AgentPacketRequest, - kind: AgentPacketKind, + kind: AgentPacketKind | null, ): Promise { const explainRequest: AgentExplainTarget = { root: request.root, - target: request.handle, + target: request.target, }; if (request.maxSymbols !== undefined) { explainRequest.maxSymbols = request.maxSymbols; @@ -76,14 +74,14 @@ 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, + schemaVersion: 2, root: explanation.root, - handle: request.handle, - kind, + target: request.target, + kind: kind ?? packetKindForResolvedTarget(explanation.target.kind), packet: explanation, limits: explanation.limits, omittedCounts: explanation.omittedCounts, @@ -91,18 +89,20 @@ 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 | 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"; } 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, @@ -110,9 +110,9 @@ 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..c0d9c149 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 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. @@ -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..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/src/cli/orient.ts b/src/cli/orient.ts index 60662384..356f0a8b 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, }); - if (context.hasFlag("--json") || !context.hasFlag("--pretty")) { + if (writesJson) { context.writeJSONLine(response); } else { context.writeStdoutLine(formatAgentOrientation(response)); @@ -42,23 +45,39 @@ export async function handleOrientCommand(context: OrientCommandContext): Promis } export function formatAgentOrientation(response: AgentOrientResponse): string { - const lines: string[] = ["Summary", ...response.summary.map((entry) => `- ${entry}`), "", "Tree"]; + const lines: string[] = ["Summary", ...response.summary.map((entry) => `- ${entry}`)]; + + 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}`); + } + } + } + + 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"); if (response.tree.length) { lines.push(...response.tree.map(formatTreeEntry)); } else { lines.push("- No files in scope."); } - 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}`); - } - } - - lines.push("", "Recommended next"); - for (const next of response.recommendedNext) { - lines.push(`- ${next.command}`); + 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..61ecc9af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -225,7 +225,7 @@ 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, @@ -233,12 +233,13 @@ export type { AgentOrientHealthMode, AgentOrientRequest, AgentOrientResponse, + AgentOrientationFocus, 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..9a05d28e 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, symbol name, SQL object name, 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..80273eed 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 { @@ -19,20 +20,40 @@ 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); }); + 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"); @@ -51,6 +72,26 @@ 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); + 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 () => { const root = await mkTmpDir("cg-agent-orient-dot-"); await writeFile(root, "src/index.ts", "export const value = 1;\n"); @@ -58,12 +99,34 @@ 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(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); + 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 +138,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..760f4cdc 100644 --- a/tests/agent-packet.test.ts +++ b/tests/agent-packet.test.ts @@ -12,23 +12,37 @@ 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.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 = ` @@ -49,7 +63,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 +72,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,14 +91,12 @@ 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.schemaVersion).toBe(2); expect(packet.kind).toBe("review"); expect(packet.followUps.some((command) => command.includes("codegraph review"))).toBeTruthy(); if (packet.packet.schemaVersion !== 2) { @@ -98,8 +110,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..160313a1 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 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"); }); test("search command prints usage before running without a query", async () => { @@ -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( @@ -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..eff64906 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,34 +1441,46 @@ 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"); + expect(stdout.split("codegraph packet get").length - 1).toBe(1); }); - 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; 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 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..fa1c8484 100644 --- a/tests/mcp-server.test.ts +++ b/tests/mcp-server.test.ts @@ -287,12 +287,12 @@ 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.schemaVersion).toBe(2); expect(packet.kind).toBe("file"); expect(JSON.stringify(packet.packet)).toContain("src/run.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); @@ -311,6 +313,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"], }, {