Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1949166
Add MCP freshness checks
lzehrung Jul 3, 2026
b8bb529
Fix MCP freshness review findings
lzehrung Jul 3, 2026
054a213
Harden SQLite artifact freshness
lzehrung Jul 3, 2026
2216acd
Fix SQLite artifact freshness edge cases
lzehrung Jul 3, 2026
84531fb
Complete SQLite artifact freshness hardening
lzehrung Jul 3, 2026
1ee8780
Close SQLite freshness review gaps
lzehrung Jul 3, 2026
1a07246
Cover SQLite freshness edge cases
lzehrung Jul 3, 2026
5c1aa8e
Add explore facade
lzehrung Jul 3, 2026
86a7083
Add agent installer workflow
lzehrung Jul 3, 2026
30cf07f
Add project lifecycle commands
lzehrung Jul 3, 2026
ced65d1
Fix lifecycle freshness and force behavior
lzehrung Jul 3, 2026
3dc25dd
Address lifecycle review findings
lzehrung Jul 3, 2026
4bd1dbb
Resolve lifecycle verify regressions
lzehrung Jul 3, 2026
9b911f4
Complete lifecycle review coverage
lzehrung Jul 3, 2026
ac97209
Normalize lifecycle option drift
lzehrung Jul 3, 2026
a8472f5
Cover lifecycle option defaults
lzehrung Jul 3, 2026
674abe1
Merge origin/main into p1/lifecycle-commands
lzehrung Jul 8, 2026
8a7f827
Fix lifecycle sync diffing, config drift, and root test coverage
lzehrung Jul 8, 2026
14841f2
Centralize config drift hashing and strengthen content-drift coverage
lzehrung Jul 8, 2026
4bf6920
Surface config hash errors and cover disk cache invalidation
lzehrung Jul 8, 2026
f19d53e
Cover lifecycle config hash warning path
lzehrung Jul 8, 2026
07cf92c
Address Copilot review: lazy status I/O, metadata file signatures, st…
lzehrung Jul 8, 2026
9a034f1
Address Copilot review: drop non-functional --cache from lifecycle co…
lzehrung Jul 8, 2026
d2534b6
Fix: restore --cache in EXPLORE_HELP_TEXT, only accurate for explore
lzehrung Jul 8, 2026
935c4ab
Address Copilot review: make stableStringify total, relax brittle cac…
lzehrung Jul 8, 2026
5444ce5
Address Copilot review: wrap uninit dir errors, drop no-op --pretty, …
lzehrung Jul 9, 2026
ddbe388
Address Copilot review + proactive self-review: consistent totalDelta…
lzehrung Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Detailed command contracts and JSON shapes live in [docs/cli.md](./docs/cli.md).
- 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 exploration, 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.
- Project lifecycle commands initialize, inspect, refresh, and remove `.codegraph/manifest.json` metadata while reusing the existing disk cache.
- 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.
Expand Down Expand Up @@ -96,6 +97,10 @@ node ./dist/cli.js orient --root . --budget small --pretty
node ./dist/cli.js search "build review report" --json
node ./dist/cli.js explain src/cli.ts

# optional lifecycle manifest and cache warmup
node ./dist/cli.js init --root .
node ./dist/cli.js status --root . --json

# optional runtime and artifact health check
node ./dist/cli.js doctor

Expand Down Expand Up @@ -140,6 +145,11 @@ codegraph orient --root . --budget small --pretty
codegraph search "build review report" --json
codegraph explain src/review.ts

# project lifecycle marker and cache warmup
codegraph init --root .
codegraph status --root . --json
codegraph sync --root .

# semantic navigation
codegraph goto <file> <line> <column>
codegraph refs --file src/index.ts --line 12 --col 17 --pretty
Expand Down
3 changes: 3 additions & 0 deletions codegraph-skill/codegraph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ Then choose the smallest useful follow-up:
- review: `codegraph review --base HEAD --head WORKTREE --summary`
- drift: `codegraph drift ./src --base origin/main --head HEAD --pretty --graph-edges summary --public-api removals`
- installer: `codegraph install --target codex,claude --dry-run`
- lifecycle: `codegraph init --root .`, `codegraph status --root . --json`, `codegraph sync --root .`

Use `--root` to define the project boundary for config lookup, cache scope, path confinement, and output normalization.
For `orient`, `drift`, and positional graph commands, positional paths are include roots inside that project.
Use `codegraph install --target <ids> --yes` to configure supported local agent clients with MCP entries, bundled skill payloads, and marker files. Use `--dry-run` or `--print-config <target>` first; uninstall removes only Codegraph-owned marker blocks, marker files, exact bundled skill payloads, or exact installer-owned MCP entries.
Lifecycle commands own only `.codegraph/manifest.json` metadata. `init` and `sync` may warm or update `.codegraph-cache/index-v1/`; other commands do not depend on the manifest. Use `uninit` to remove recognized lifecycle state.
Lifecycle commands accept either a positional project path or `--root <path>`; never combine both.

## Output Choice

Expand Down
14 changes: 14 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ codegraph graph --root . --sql-artifacts --json
# SQLite export
codegraph graph --sqlite ./codegraph.sqlite

# Project lifecycle marker and cache warmup
codegraph init --root .
codegraph status --root . --json
codegraph sync --root .
codegraph uninit --root . --force

# Build and report diagnostics
codegraph graph --report
codegraph index --report
Expand All @@ -111,6 +117,14 @@ codegraph review --report --report-file review.report.json

Graph, index, and review reports include `backend.native.byLanguage` so native usage and fallback remain visible per language. Build reports also include `backend.parser` when syntax-tree backend degradation leaves files without parser context. Reports also include `graph.fallbackImportExtraction.byLanguage` and `byReason` when regex import extraction is used. Review JSON reports `diagnostics.symbolMappingParseFailures`, `diagnostics.missingFiles`, `changedFiles[].status` as `updated`, `deleted`, or `missing`, and `sqlContext` when changed SQL files or changed SQL literals make SQL artifact facts relevant.

### Project lifecycle

- `init` creates `.codegraph/manifest.json`, warms the existing disk cache through the index build path, and is idempotent when the manifest is current. Use `--force` to rebuild and overwrite the manifest metadata.
- `status` reports whether lifecycle metadata exists, last sync time, then/current file counts, per-file content drift (files changed even when counts match, e.g. edits in place or N files swapped for N others), config/build-option drift, analysis label, and the suggested next command. Use `--json` for `schemaVersion: 1`.
- `sync` refreshes the manifest after edits and requires an initialized project unless `--init` is passed.
- `uninit` removes only recognized lifecycle state by default. It refuses unknown `.codegraph/` entries unless `--force` is passed.
- Lifecycle commands accept either a positional project path or `--root <path>`. They reject using both together because lifecycle manifests always describe one project boundary, not include-root subsets.

### Symbols, navigation, grep, and chunking

```bash
Expand Down
47 changes: 46 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { handleGrepCommand } from "./cli/grep.js";
import { CLI_HELP_TEXT, helpTextForCommand, isKnownCliCommand } from "./cli/help.js";
import { handleImpactCommand } from "./cli/impact.js";
import { handleInstallerCommand } from "./cli/install.js";
import { handleLifecycleCommand } from "./cli/lifecycle.js";
import { CodegraphLifecycleUserError } from "./lifecycle/manifest.js";
import { handleIndexCommand } from "./cli/index.js";
import { handleHotspotsCommand, handleInspectCommand } from "./cli/inspect.js";
import { handleOrientCommand } from "./cli/orient.js";
Expand Down Expand Up @@ -80,6 +82,10 @@ function isExistingDirectory(filePath: string): boolean {
}
}

function isLifecycleCommand(command: string): command is "init" | "status" | "sync" | "uninit" {
return command === "init" || command === "status" || command === "sync" || command === "uninit";
}

function assertValidIncludeRoots(command: string, baseRoot: string, includeRoots: readonly string[]): void {
const globLikeRoot = includeRoots.find((includeRoot) => looksLikeGlobPattern(baseRoot, includeRoot));
if (!globLikeRoot) return;
Expand Down Expand Up @@ -213,6 +219,25 @@ async function runCliWithActiveRuntime(rawArgs: string[]) {
}

const firstPositionalRoot = parsed.positionals.length === 1 ? resolveAbs(parsed.positionals[0]!) : undefined;
if (isLifecycleCommand(cmd) && rootOpt && parsed.positionals.length) {
writeStderrLine(
`Invalid ${cmd} path "${parsed.positionals[0]!}". Positional paths cannot be combined with --root for lifecycle commands.`,
);
exitCli(2);
return;
}
if (
isLifecycleCommand(cmd) &&
!rootOpt &&
firstPositionalRoot !== undefined &&
!isExistingDirectory(firstPositionalRoot)
) {
writeStderrLine(
`Invalid ${cmd} path "${parsed.positionals[0]!}". Expected an existing directory or use --root <path>.`,
);
exitCli(2);
return;
}
const defaultProjectRoot =
(cmd === "graph" ||
cmd === "graph-delta" ||
Expand All @@ -221,7 +246,8 @@ async function runCliWithActiveRuntime(rawArgs: string[]) {
cmd === "hotspots" ||
cmd === "inspect" ||
cmd === "duplicates" ||
cmd === "impact") &&
cmd === "impact" ||
isLifecycleCommand(cmd)) &&
!rootOpt &&
firstPositionalRoot !== undefined &&
isExistingDirectory(firstPositionalRoot)
Expand Down Expand Up @@ -538,6 +564,25 @@ async function runCliWithActiveRuntime(rawArgs: string[]) {
return await resolveFilesFromRoots();
};

if (isLifecycleCommand(cmd)) {
try {
await handleLifecycleCommand({
command: cmd,
root: projectRootFs,
buildOptions: buildAgentOptions(),
hasFlag,
writeJSONLine,
writeStdoutLine,
});
} catch (error) {
if (error instanceof CodegraphLifecycleUserError) {
writeStderrLine(error.message);
exitCli(1);
}
throw error;
}
return;
}
if (cmd === "explore") {
await handleExploreCommand({
positionals: parsed.positionals,
Expand Down
31 changes: 31 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Commands:
drift Compare architecture health between refs or artifacts
mcp Serve MCP tools for agent graph navigation
index Build the project symbol index
init Initialize project-local Codegraph lifecycle metadata
status Inspect project-local Codegraph lifecycle metadata
sync Refresh project-local Codegraph lifecycle metadata
uninit Remove project-local Codegraph lifecycle metadata
goto Go to definition
refs Find references
deps List dependencies
Expand Down Expand Up @@ -84,6 +88,10 @@ Examples:
codegraph explore "how does auth reach db?" --pretty
codegraph explain src/auth.ts --json
codegraph impact --provider git --base HEAD --head WORKTREE
codegraph init --root .
codegraph status --root . --json
codegraph sync --root .
codegraph uninit --root . --force
codegraph packet get file:src%2Fcli.ts --json
codegraph artifact build --root . --out codegraph-out --json
codegraph mcp serve --root . --stdio
Expand Down Expand Up @@ -129,6 +137,7 @@ const knownCliCommands = new Set([
"hotspots",
"impact",
"index",
"init",
"install",
"inspect",
"mcp",
Expand All @@ -140,8 +149,11 @@ const knownCliCommands = new Set([
"review",
"search",
"skill",
"status",
"sync",
"sql",
"unresolved",
"uninit",
"version",
"uninstall",
]);
Expand All @@ -150,6 +162,23 @@ export function isKnownCliCommand(command: string): boolean {
return knownCliCommands.has(command);
}

export const LIFECYCLE_HELP_TEXT = `codegraph init/status/sync/uninit - Initialize, inspect, refresh, or remove project-local Codegraph state

Usage:
codegraph init [path] [--force] [--json]
codegraph init --root <path> [--force] [--json]
codegraph status [path] [--json]
codegraph status --root <path> [--json]
codegraph sync [path] [--init] [--json]
codegraph sync --root <path> [--init] [--json]
codegraph uninit [path] [--force] [--json]
codegraph uninit --root <path> [--force] [--json]

State:
Lifecycle commands own only .codegraph/manifest.json metadata. Init and sync may warm or update the disk cache under .codegraph-cache/index-v1/. Other commands do not depend on the manifest.
Positional paths and --root are alternatives for lifecycle commands; do not combine them.
`;

export const INSTALL_HELP_TEXT = `codegraph install - Configure Codegraph for supported agent clients

Usage: codegraph install [target] [--target <codex,claude,cursor,gemini,opencode,agents>] [--yes | --dry-run] [--print-config <target>] [--detect]
Expand Down Expand Up @@ -389,6 +418,8 @@ export function helpTextForCommand(command: string, positionals: readonly string
if (command === "explain") return EXPLAIN_HELP_TEXT;
if (command === "install") return INSTALL_HELP_TEXT;
if (command === "uninstall") return UNINSTALL_HELP_TEXT;
if (command === "init" || command === "status" || command === "sync" || command === "uninit")
return LIFECYCLE_HELP_TEXT;
if (command === "drift") return DRIFT_HELP_TEXT;
if (command === "duplicates") return DUPLICATES_HELP_TEXT;
if (command === "artifact") return ARTIFACT_HELP_TEXT;
Expand Down
96 changes: 96 additions & 0 deletions src/cli/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { BuildOptions } from "../indexer/types.js";
import {
getCodegraphLifecycleStatus,
initCodegraphLifecycle,
syncCodegraphLifecycle,
uninitCodegraphLifecycle,
type CodegraphLifecycleStatus,
type CodegraphLifecycleSyncResult,
type CodegraphLifecycleUninitResult,
} from "../lifecycle/manifest.js";

export type LifecycleCommandContext = {
command: "init" | "status" | "sync" | "uninit";
root: string;
buildOptions?: BuildOptions;
hasFlag: (name: string) => boolean;
writeJSONLine: (value: unknown) => void;
writeStdoutLine: (message: string) => void;
};

export async function handleLifecycleCommand(context: LifecycleCommandContext): Promise<void> {
if (context.command === "init") {
const result = await initCodegraphLifecycle(context.root, {
...(context.buildOptions ? { buildOptions: context.buildOptions } : {}),
force: context.hasFlag("--force"),
});
writeLifecycleResult(context, result, formatSyncResult("Initialized", result));
return;
}

if (context.command === "sync") {
const result = await syncCodegraphLifecycle(context.root, {
...(context.buildOptions ? { buildOptions: context.buildOptions } : {}),
init: context.hasFlag("--init"),
});
writeLifecycleResult(context, result, formatSyncResult("Synced", result));
return;
}

if (context.command === "uninit") {
const result = await uninitCodegraphLifecycle(context.root, { force: context.hasFlag("--force") });
writeLifecycleResult(context, result, formatUninitResult(result));
return;
}

const result = await getCodegraphLifecycleStatus(context.root, {
...(context.buildOptions ? { buildOptions: context.buildOptions } : {}),
});
writeLifecycleResult(context, result, formatStatus(result));
}

function writeLifecycleResult(
context: LifecycleCommandContext,
value: CodegraphLifecycleStatus | CodegraphLifecycleSyncResult | CodegraphLifecycleUninitResult,
pretty: string,
): void {
if (context.hasFlag("--json")) {
context.writeJSONLine(value);
} else {
context.writeStdoutLine(pretty);
}
}

function formatSyncResult(label: string, result: CodegraphLifecycleSyncResult): string {
const { added, removed } = result.changedFiles;
// Report added/removed explicitly rather than the net delta alone: equal adds and removes
// cancel out to a delta of 0, which would otherwise hide real file churn.
const changeLabel = added || removed ? `, +${added}/-${removed}` : "";
return `${label} Codegraph at ${result.root}: ${result.manifest.fileCount} files${changeLabel}. Manifest: ${result.manifestPath}`;
}

function formatUninitResult(result: CodegraphLifecycleUninitResult): string {
if (!result.removed) return `Codegraph is not initialized at ${result.root}.`;
return `Removed Codegraph lifecycle state at ${result.root}.`;
}

function formatStatus(status: CodegraphLifecycleStatus): string {
if (!status.initialized) {
return `Codegraph is not initialized at ${status.root}. Next: ${status.suggestedNextCommand}`;
}
const fileCount = status.fileCount ? `${status.fileCount.then} then, ${status.fileCount.current} current` : "unknown";
const filesChanged = status.filesChanged ? "yes" : "no";
const configChanged = status.configChanged ? "yes" : "no";
const buildOptionsChanged = status.buildOptionsChanged ? "yes" : "no";
const analysis = status.analysis?.label ?? "unknown";
return [
`Codegraph initialized at ${status.root}.`,
`Last sync: ${status.lastSyncAt ?? "unknown"}`,
`Files: ${fileCount}`,
`Files changed: ${filesChanged}`,
`Config changed: ${configChanged}`,
`Build options changed: ${buildOptionsChanged}`,
`Analysis: ${analysis}`,
`Next: ${status.suggestedNextCommand}`,
].join("\n");
}
Comment on lines +77 to +96

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ddbe388: formatStatus now includes a 'Files changed: yes/no' line. While in the area, also fixed the closely related issue in formatSyncResult: it previously showed only the net delta, so e.g. 3 files added and 3 removed in the same sync produced totalDelta: 0 and the change label was suppressed entirely (deltaLabel was only shown when non-zero), silently hiding real churn. It now prints explicit +/- instead. Regression tests cover both via captureCli against the actual pretty-printed stdout (no --json).

44 changes: 44 additions & 0 deletions src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ const SHARED_BUILD_OPTIONS = [
"--ignore-glob",
"--resolution-hint",
];
// Lifecycle commands (init/status/sync) always warm/read the disk cache and never honor an
// explicit --cache override, so --cache is intentionally excluded here to keep the CLI
// contract truthful.
const LIFECYCLE_BUILD_OPTIONS = SHARED_BUILD_OPTIONS.filter((option) => option !== "--cache");
// `status` never calls createAgentSession/loadProject (it only hashes config, hashes build
// options, and lists project files for signature hashing), so --cache-verify, --progress, and
// --workers have no observable effect there, unlike for init/sync which do a full build.
const STATUS_BUILD_FLAGS = SHARED_BUILD_FLAGS.filter(
(flag) => flag !== "--cache-verify" && flag !== "--progress" && flag !== "--workers",
);
// --threads only matters for the concurrency of an actual build (init/sync); status never builds.
const STATUS_BUILD_OPTIONS = LIFECYCLE_BUILD_OPTIONS.filter((option) => option !== "--threads");
const JSON_OUTPUT_FLAGS = ["--json", "--pretty"];
const REPORT_FLAGS = ["--report"];
const REPORT_OPTIONS = ["--report-file"];
Expand Down Expand Up @@ -361,6 +373,14 @@ const CLI_COMMAND_SCHEMAS = new Map<string, CliCommandSchema>([
),
],
["index", graphCommandSchema({ kind: "any" })],
[
"init",
commandSchema([...SHARED_BUILD_FLAGS, "--json", "--force"], LIFECYCLE_BUILD_OPTIONS, {
kind: "max",
max: 1,
usage: "Usage: codegraph init [path] [--force] [--json] OR codegraph init --root <path> [--force] [--json]",
}),
],
Comment thread
Copilot marked this conversation as resolved.
[
"install",
commandSchema(["--detect", "--dry-run", "--yes"], ["--print-config", "--target"], {
Expand Down Expand Up @@ -481,13 +501,37 @@ const CLI_COMMAND_SCHEMAS = new Map<string, CliCommandSchema>([
usage: "Usage: codegraph skill <install|print-path|doctor> [--agent <name> | --target <dir>] [--force]",
}),
],
[
"status",
commandSchema([...STATUS_BUILD_FLAGS, "--json"], STATUS_BUILD_OPTIONS, {
kind: "max",
max: 1,
usage: "Usage: codegraph status [path] [--json] OR codegraph status --root <path> [--json]",
}),
],
Comment thread
Copilot marked this conversation as resolved.
[
"sync",
commandSchema([...SHARED_BUILD_FLAGS, "--json", "--init"], LIFECYCLE_BUILD_OPTIONS, {
kind: "max",
max: 1,
usage: "Usage: codegraph sync [path] [--init] [--json] OR codegraph sync --root <path> [--init] [--json]",
}),
],
Comment thread
Copilot marked this conversation as resolved.
[
"sql",
commandSchema(["--json"], ["--db", "--query", "--sqlite"], {
kind: "none",
usage: 'Usage: codegraph sql --db <sqlite path> --query "SELECT ..."',
}),
],
[
"uninit",
commandSchema(["--force", "--json"], ["--root"], {
kind: "max",
max: 1,
usage: "Usage: codegraph uninit [path] [--force] [--json] OR codegraph uninit --root <path> [--force] [--json]",
}),
],
[
"unresolved",
commandSchema([...SHARED_BUILD_FLAGS, ...JSON_OUTPUT_FLAGS, "--verbose"], SHARED_BUILD_OPTIONS, {
Expand Down
Loading