fix(sync): filesystem-based change detection (catch git pull & non-git edits) (#414)

* fix(sync): detect changes via filesystem, not git status

Incremental sync detected changes with `git status --porcelain`, which only sees uncommitted working-tree changes — so committed changes from git pull/checkout/merge/rebase (clean tree afterward) were never reconciled, and non-git projects leaned on a slow full rescan. Change detection is now filesystem-based and git-independent: a (size, mtime) stat pre-filter skips unchanged files, then a content hash confirms the rest; removals are checked against the filesystem (git ls-files still lists deleted-but-unstaged files). Also adds a non-blocking catch-up sync on MCP connect so changes made while the server was down (e.g. a terminal git pull) are reconciled on connect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): add 0.9.5 entry for filesystem-based sync fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-25 17:36:09 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 1be8e7830f
commit 4a94696e44
3 changed files with 108 additions and 77 deletions
+28
View File
@@ -243,6 +243,7 @@ export class MCPServer {
this.cg = await CodeGraph.open(resolvedRoot);
this.toolHandler.setDefaultCodeGraph(this.cg);
this.startWatching();
this.catchUpSync();
} catch (err) {
// Log the error so transient failures are diagnosable (see issue #47)
const msg = err instanceof Error ? err.message : String(err);
@@ -301,6 +302,7 @@ export class MCPServer {
this.projectPath = resolvedRoot;
this.toolHandler.setDefaultCodeGraph(this.cg);
this.startWatching();
this.catchUpSync();
} catch {
// Still failing — will retry on next tool call
}
@@ -370,6 +372,32 @@ export class MCPServer {
}
}
/**
* Reconcile the index with the current filesystem once, right after connect —
* catches edits, adds, deletes, and `git pull`/`checkout` changes made while
* no watcher was running. Runs in the background so it never delays the
* `initialize` response; `sync()` is incremental (a stat pre-filter skips
* unchanged files) and mutex-guarded, so it can't collide with the live
* watcher or a git-hook sync. Runs even when the watcher is unavailable
* (e.g. WSL2 /mnt drives), where catch-up matters most.
*/
private catchUpSync(): void {
const cg = this.cg;
if (!cg) return;
void cg
.sync()
.then((result) => {
const changed = result.filesAdded + result.filesModified + result.filesRemoved;
if (changed > 0) {
process.stderr.write(`[CodeGraph MCP] Caught up ${changed} file(s) changed since last run\n`);
}
})
.catch((err) => {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`[CodeGraph MCP] Catch-up sync failed: ${msg}\n`);
});
}
/**
* Stop the server
*/