Commit Graph
100 Commits
Author SHA1 Message Date
2176a7a439 fix(extraction): record instantiates for C++ stack/brace construction (#1035) (#1049)
`instantiates` edges came only from heap `new Calculator(0)` (a
new_expression) and copy-init `Calculator c = Calculator(0)` (a
call_expression). Stack direct-init `Calculator calc(0)` and brace-init
`Widget w{1, 2}` parse as a `declaration` whose constructor arguments hang
directly off the declarator as an argument_list / initializer_list — there
is no call/new node — so the function-body walker saw no constructor
invocation and emitted no edge. A function that built objects with the
ordinary stack syntax looked like it didn't construct them, and the
dependency was missing from impact / callers.

In the body walker, a C++ `declaration` that is a stack/brace construction
now reuses extractInstantiation (a declaration's `type` field IS the
constructed class name, and extractInstantiation already strips template
args / namespace and emits the `instantiates` ref). Gated by
isCppStackConstruction, which requires BOTH a class-like type
(type_identifier / template_type / qualified_identifier — so `int x(0)`
and `auto z = …` are excluded) AND a declarator carrying args
(argument_list / initializer_list — so default `Calculator c;` and the
most-vexing-parse `Calculator c();` are excluded). The edge targets the
class node, not the same-named constructor method.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:12:53 -05:00
f4e03e9cdc fix(extraction): resolve C++ inheritance from templated base classes (#1043) (#1048)
A C++ class deriving from a template — `class Derived : public Base<int>`,
a CRTP base `class App : public CRTPBase<App>`, a struct inheriting a
template, or a templated base mixed into a multi-base clause — recorded its
base as the full instantiation text (`Base<int>`). That never name-matched
the template, which is indexed as the bare node `Base`, so the `extends`
edge never resolved and the derived class looked like it inherited from
nothing — callers/impact analysis stopped at the boundary.

Strip the template arguments from the base-type reference name in the
`base_class_clause` handler via a new `stripCppTemplateArgs` helper: it
removes every balanced `<…>` group (any nesting/position), so `Base<int>`
→ `Base` and `ns::Tpl<int>` → `ns::Tpl`. The remaining qualified head is
exactly what the non-templated base case already produces, so resolution
treats templated and non-templated bases identically; a name with no
template args passes through unchanged.

Covers same-file and same-namespace bases (the dominant real-world
patterns). A base in a different namespace referenced with its qualifier
(`other_ns::Tpl<int>`) still doesn't resolve, but that's a pre-existing,
orthogonal namespace-resolution gap — the non-templated `other_ns::Plain`
fails identically — not a template issue.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:43:48 -05:00
4c0c87ff6e docs(changelog): note the Windows daemon shutdown fix (#1041) (#1042)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:12:54 -05:00
cfa293539f fix(mcp): drain the event loop on Windows daemon shutdown instead of aborting mid-watcher-close (#1041)
On Windows, calling process.exit() while a recursive fs.watch handle is still
tearing down aborts the daemon with a libuv UV_HANDLE_CLOSING assertion
(0xC0000409) — reproducible whenever the indexed tree contains a nested repo
(submodule / embedded clone), since that's what keeps a watch active at shutdown.
A small exit delay doesn't help; only letting the loop drain is clean (verified
on a real Windows VM: close()+exit() and close()+setTimeout(exit) both abort,
while letting the loop drain exits 0).

finalizeDaemonExit() now exits immediately on POSIX (unchanged) but on Windows
marks success (exitCode=0) and lets the loop drain to a natural exit, with an
unref'd backstop that force-exits only if a stray handle would otherwise hang
shutdown. The daemon's own timers are already unref'd and its PPID watchdog lives
in the proxy, so nothing keeps the loop alive past the closing watch handles —
natural drain is fast. Pure + platform-injected so both branches unit-test off-Windows.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:11:05 -05:00
f73227d2f5 fix(mcp): don't warn "different git working tree" for submodules covered by the parent index (#1031, #1033) (#1039)
Indexing a super-repo now descends into its submodules and gitlinked clones, so
a query run from inside one resolves up to the parent's unified index — whose
graph DOES contain that nested repo's files. But the git-worktree-mismatch
warning still fired, telling the agent the results were from "a different working
tree" and to run `codegraph init -i` — which would split the submodule back into
its own index and undo the unified view. A false positive carrying harmful advice.

Distinguish a genuine borrowed worktree (the SAME repository on a different
branch — shares a git common dir with the index root) from a submodule/embedded
clone (a DIFFERENT repository — its own common dir), and suppress the warning
only for the latter. Add gitCommonDir() for the check. The issue-#155 linked-worktree
case is unchanged.

Verified end-to-end: the warning no longer fires for a submodule-rooted MCP
session and still fires for a real linked worktree. Edit-sync (manual sync +
the live watcher) keeps the nested repo's files current on both macOS and Linux
(active-submodule and bare-gitlink shapes), so suppressing the warning is safe.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:35:17 -05:00
a4dfc3438f fix(extraction): index nested repos recorded as gitlinks (#1031, #1033) (#1038)
A nested git repo tracked as a gitlink (mode 160000) — a clone `git add`ed
into the super-repo without a `.gitmodules` entry, or a submodule that
isn't active/initialized in this checkout — fell through both file-collection
passes: it's tracked, so the untracked `-o` listing skips it, but it's not
an active submodule, so `--recurse-submodules` won't expand it. Indexing the
top level therefore pulled in only the outer repo's own files and stopped at
the nested repo's boundary (one report: ~10 files at the root).

Switch the tracked scan to `ls-files -s` to expose file modes, collect the
unexpanded 160000 entries, and recurse into each that has a real working tree
on disk as its own embedded repo. Mirror the same discovery in
discoverEmbeddedRepoRoots so the watcher's scope stays equal to the indexer's.

Active submodules (#147) and untracked nested clones (#193) are unchanged;
gitlinks under default-ignored dirs (vendor/, node_modules/) stay excluded
(#407); an uninitialized submodule with no checkout on disk is left alone.
Adds four-shape coverage in extraction.test.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:41:00 -05:00
Colby MchenryandGitHub 82b62a21e4 Bump version to 1.1.2 in package-lock.json 2026-06-27 15:49:12 -05:00
Colby MchenryandGitHub fcc275ee0a Bump version from 1.1.1 to 1.1.2 2026-06-27 15:49:00 -05:00
a79fa51816 feat(mcp): add readOnlyHint annotations so tools work in Cursor Ask mode (#1027)
All codegraph_* tools are query-only — they read the pre-built index and
never mutate the workspace — but they advertised no MCP annotations, so
Cursor's Ask mode (and any client that gates on read-only tools) blocked
every call with "you are in ask mode and cannot run non read-only tools."

Add a shared READ_ONLY_ANNOTATIONS constant (readOnlyHint: true,
destructiveHint: false, idempotentHint: true, openWorldHint: false) and
reference it from each of the 8 tool definitions. The field flows through
every tools/list path: the live getTools() (including explore's
spread-rewritten description), the static proxy getStaticTools(), and the
no-default withRequiredProjectPath schema clone.

The annotations field is additive, so it ships without bumping the
negotiated 2024-11-05 protocol version: clients that gate on it read it
regardless, and older clients ignore it.

Closes #1018

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:47:31 -05:00
9716fb27ae feat(extraction): parallelize indexing across a parse worker pool (#1015) (#1025)
indexAll parsed every file through a single worker thread, so a full `codegraph index` used one core no matter the machine. Add ParseWorkerPool (src/extraction/parse-pool.ts), modeled on the shipped QueryPool: indexAll now parses across clamp(cores-1,1,8) workers. CODEGRAPH_PARSE_WORKERS overrides the count; 1 reproduces the previous single-worker path exactly (the rollback).

Parses run concurrently but results commit to SQLite in file order. This matters: the post-index resolution phase selects among ambiguous same-named candidates by node DB-insertion order, so a stable commit order keeps the graph deterministic — byte-identical to the serial path — instead of drifting with parse-completion timing. A bounded reorder buffer (backpressure on dispatched-but-uncommitted count) keeps memory flat even if a file is slow at the commit cursor.

Crash/timeout of a worker rejects only that file's parse (feeding the existing retry pass) and respawns; per-worker recycle every 250 parses reclaims WASM heap. In-process fallback unchanged when the compiled worker is absent (tests).

Validated on real OSS (django +9%, redis +17%; modest and parse-fraction-dependent), graph byte-identical across worker counts, peak memory flat-to-lower since workers recycle independently — so the #320 OOM concern doesn't materialize. Adds 11 pool unit tests.

Closes #1015. Refs #320.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:32:50 -05:00
b3f59c717a fix(extraction): index Swift computed properties so they're findable (#1020) (#1024)
Swift in-class properties are extracted by a dedicated branch in
TreeSitterExtractor.visitNode, not the generic nameField/variableTypes path
swift.ts declares. That branch had a `!isComputed` gate that dropped computed
properties entirely, so `codegraph query`/`codegraph_explore` returned "No
results found" for them — including a SwiftUI view's `var body: some View`,
the most important symbol in any SwiftUI app, and the heavily-read
`var isCloudProxy: Bool` from the report.

Stored properties were already fixed in #708 (v1.0.0); the reporter tested
v0.9.9 and confirmed "still present on main" by inspecting swift.ts only,
missing the dedicated branch — so only the computed-property half was real.

- Computed properties now index as `property` nodes; the getter is walked via
  visitFunctionBody so its calls attribute to the property (a SwiftUI `body`'s
  subview tree becomes the property's callees — the render flow is traceable
  through it), not flattened onto the enclosing type.
- Protocol property requirements (`var x: T { get }`) — a third never-indexed
  category — index as `property` too.
- Routing the getter through visitFunctionBody also stops getter-local
  `let`/`var` declarations from being wrongly node-ified as struct fields
  (the generic child-walk used to do this): Alamofire property 0→348, field
  618→588, idempotent.

Stored/static behavior is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:21:00 -05:00
30dc303f4c fix(db): chunk deleteResolvedReferences IN-list under the SQLite param limit (#1001) (#1023)
deleteResolvedReferences bound every id into a single unbounded
`IN (...)`, so a list longer than SQLITE_MAX_VARIABLE_NUMBER (32766 on
the bundled node:sqlite) threw "too many SQL variables" — the one IN-list
in queries.ts that #540 missed. It's reachable only through the exported
QueryBuilder (library use): the internal resolution path uses
deleteSpecificResolvedReferences, which binds per-row and is immune, so
the CLI/MCP indexing pipeline was never affected. Wrap it in the same
SQLITE_PARAM_CHUNK_SIZE loop every sibling query uses, and add a
regression test (33k ids, past the real 32766 ceiling) that throws
without the fix.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:00:55 -05:00
f83a1ecc8e fix(mcp): start the daemon on ExFAT/FAT/network filesystems (#997) (#1022)
A project kept on an ExFAT/FAT external volume (or some network mounts /
WSL2 DrvFs) broke the background auto-sync daemon at two points, both
because the filesystem lacks POSIX features the daemon relied on:

1. Lock acquisition hard-links a temp file onto .codegraph/daemon.pid for
   race-free exclusivity (#411) — these filesystems have no hard links.
2. The Unix-domain socket listen() fails regardless of path length, so the
   old length-only tmpdir fallback never triggered.

Both surface as a capability error, but each OS reports a DIFFERENT errno
for the same gap (macOS ENOTSUP, Linux EPERM, Windows EISDIR), so the fix
is policy-based rather than an enumerated code-set:

- Lock: fall back to an O_EXCL create on any non-EEXIST link error. The
  temp write already proved the directory is writable, so the fallback
  either succeeds (still atomic + exclusive, "first writer wins") or
  surfaces its own genuine error.
- Socket: an ordered candidate list [in-project, tmpdir] walked by BOTH
  the daemon (binds) and the proxy (connects) — they converge on the
  fallback with zero coordination. Relocate past any non-EADDRINUSE bind
  error; EADDRINUSE still rethrows, preserving the #974 contract.

Normal repos are unaffected: the in-project candidate binds first, and the
hard-link lock path is unchanged.

Validated end-to-end on real removable-drive filesystems: macOS ExFAT
(hdiutil image), Linux FAT32 (Docker loop mount), Windows exFAT (diskpart
VHD) — each acquires the lock, relocates (or binds a named pipe on
Windows), and serves a real client.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:47:29 -05:00
Colby MchenryandGitHub 7a361ef16e docs: document the exclude codegraph.json option (#999) (#1010)
#1009 added `exclude` (keep git-tracked dirs out of the index) but didn't
document it. Add an "Excluding a tracked directory" section to the site config
page (parallel to includeIgnored) and a brief note + example to the README,
covering the committed-theme/SDK case .gitignore can't handle.
2026-06-26 20:30:24 -05:00
45d3293c6a fix(resolution): stop "Resolving refs" wedge on theme-vendoring repos; add exclude config + index watchdogs (#999) (#1009)
Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under
static/, ~1,600 tracked files):

1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped
   import-name collisions; this caps method-name collisions (init/update/render
   re-declared on every widget), which flow through matchMethodCall Strategy 3
   and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env
   CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline
   rather than score K candidates — no proximity score can pick the one true
   target among thousands anyway. Resolving drops from O(K^2) to linear in refs
   (e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap
   never fires on normal repos (max real method-collision ~40).

2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the
   index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the
   existing includeIgnored plumbing across the git, sync, and non-git-walk
   paths.

3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which
   were serve-only), so a wedged or orphaned indexer self-terminates instead of
   pinning a core. The --liftoff-only relaunch's spawnSync can't forward
   signals, so killing the parent shim used to orphan the worker.

Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git +
non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the
ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:25:47 -05:00
d3179f5004 feat(mcp): require projectPath when the MCP server has no default project (#993) (#1007)
When the server runs with no default project to fall back to — a gateway
server started outside any repo, or a monorepo root whose .codegraph/
indexes live only in sub-projects — every tool call must carry an explicit
projectPath. Previously projectPath was always optional, so an agent talking
to such a server would omit it, get success-shaped "pass projectPath"
guidance, and not reliably retry; the user had to nudge it by hand.

getTools() now marks projectPath required in the exposed tool schemas on the
no-default-project branch (a high-salience channel clients surface/validate,
unlike the instructions prose the reporter found too weak). When a default
project is open, projectPath stays optional and a bare call falls back to it.

The fix lives at the MCP schema layer, not the Claude-only front-load hook:
the hook is local-filesystem-based and never runs for the reporter (they're
on AGENTS.md / Codex-opencode). The proxy/getStaticTools path is untouched —
index.ts forces direct mode whenever resolveDaemonRoot is null, so the
no-default case never reaches the proxy.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:31:12 -05:00
b45f309a1b fix(prompt-hook): fire front-load hook for non-English prompts (#994) (#1004)
The UserPromptSubmit hook's structural-prompt gate was English-only, so a
structural question written in Chinese — or any non-Latin script — silently
injected nothing: JS `\b` is ASCII-only and never matches between Han
characters, so the keyword regex couldn't fire (and couldn't be extended in
place). To the user the hook looked unwired, with no error to explain why.

Make the gate language-aware, split into tested helpers in directory.ts:
- hasStructuralKeyword: English (\b-guarded) + CJK structural keywords.
- extractCodeTokens: identifier-shaped tokens (camelCase / snake_case /
  name() / a.b) in any language — verified against the index via
  getNodesByName before firing, so a tech brand like `JavaScript` that looks
  like a symbol but isn't one here doesn't inject ~16KB of spurious context.
- isStructuralPrompt: the cheap candidate gate (keyword OR code-token).

Adds 21 unit tests for the gate (previously untested) covering the reporter's
verification table plus the false-positive guards.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:39:34 -05:00
703629edc3 feat(c/c++): resolve function-pointer command tables — macro-built, conditional-compilation & bare arrays (#991) (#1003)
* feat(c/c++): resolve macro-built function-pointer command tables (#991)

C/C++ commands dispatched through macro-built function-pointer tables were
dead-ends in the graph: redis' `call` never showed up as a caller of any
command (`c->cmd->proc(c)`), because the table is generated into a #included
`.def`, the handler is buried inside `MAKE_CMD(...)`, the struct type is itself
a macro alias, the `proc` field uses a function-TYPE typedef, and the receiver
is a chained field access. #954 deferred exactly this shape.

Six composable additions to c-fnptr-synthesizer.ts close it:
- function-type typedefs (`typedef RET T(...)` + `T *f`) flag the field as a
  function pointer;
- multi-declarator fields (`struct redisCommand *cmd, *last`) each count as a
  slot/type (needed for positional alignment and the chain walk);
- chained/array receivers (`c->cmd->proc`) resolve through field types across
  all same-named struct layouts (redis has two unrelated `client` structs);
- `#include "x"` directives are followed (from raw source) so a non-indexed
  `.def` is read as a registration unit with the includer's effective macro env;
- function-like + object-like macros are expanded (params->args, type aliases)
  before positional/designated registration;
- a macro that expands to a brace-wrapped element (sqlite `FUNCTION(...)`) has
  one outer brace layer peeled.

Validated on two independent macro-table lineages at 100% target precision:
redis (209 commands via redisCommand.proc, `call`->every command) and sqlite
(69 FuncDef.xSFunc targets). No regression on the controls: git (cmd_struct.fn,
138 builtins), curl (Curl_cftype.*), lua (0). 0 non-function targets across all
five; +3 synthetic fixtures; full suite green.

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

* feat(c/c++): resolve conditional-compilation command tables (vim) (#991)

Vim's `:ex` and normal-mode command tables are the hardest fn-pointer-table
shape: the struct is defined INLINE with the array, the whole thing is behind
`#ifdef DO_DECLARE_EXCMD`/`DO_DECLARE_NVCMD` (switched on by the includer), built
by a macro the file conditionally redefines (`EXCMD`/`NVCMD` = the table element
under the switch, a bare enum id otherwise), and dispatched by a parenthesized
array subscript through a file-scope table: `(cmdnames[i].cmd_func)(&ea)`.

Four more composable additions on top of the macro-table work:
- a focused `#ifdef`/`#ifndef`/`#if defined`/`#else`/`#elif`/`#endif` evaluator
  drops inactive arms (unevaluable `#if EXPR` keeps its body); an indexed header
  is re-scanned in an includer's context only when that includer #defines a
  switch the header guards, with the include's macros re-read from the resolved
  text (the plain last-wins parse picks the wrong, enum, arm);
- inline `struct TAG {…} var[] = {…}` tables whose struct never became a node are
  parsed in place and registered;
- array-subscript receivers (`tbl[i].f`) strip the subscript and resolve the
  base through a global-var → struct-type map;
- an optional `)` before the call covers the parenthesized `(….f)(args)` form.

Validated on vim: 273 `:ex` commands (`do_one_cmd`→every command) + 67
normal-mode commands, 0 non-function targets, 0 cross-table misroute (registering
both tables is what stops `normal_cmd`'s `nv_cmds[i].cmd_func` from falling back
to the `cmdname` owner of the shared field name). Controls unchanged at 0
non-function (redis/sqlite/git/curl gain coverage from array/global dispatch, lua
still 0); +1 synthetic fixture; full suite green.

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

* feat(c/c++): resolve bare arrays of function pointers (#991)

The C/C++ fn-pointer synthesizer keyed everything on (struct type,
fn-pointer field), so a dispatch through a bare array of function
pointers — no struct, no field — was unbridged: an opcode/handler table
like `static op_t *opcodes[256] = {nop,…}` invoked `opcodes[op](…)` left
every handler with zero callers. Closes the last #991 deferred item.

Keyed by the array VARIABLE name (a new `arrayReg`, parallel to the
struct `reg`). Registration detects an array whose element type is a
function typedef — a function-TYPE typedef element (`opcode_t *ops[]`,
the `*` making it an array of pointers) or a function-pointer typedef
element (`zend_rc_dtor_func_t t[]`) — and reads its literal entries,
whether positional (`fn`/`&fn`), designated by index (`[IDX]=fn`), or
cast-wrapped (`(cast)fn`). Dispatch is `tbl[i](…)` / `(*tbl[i])(…)`,
gated on `tbl` being a known fn-pointer array (the precision anchor);
the fan-out reaches the whole set (a runtime subscript hits any entry),
like a command table. The same-file table wins on a name collision, so
two file-local `static opcodes[256]` (SameBoy's CPU + disassembler)
never cross. The fn-pointer typedef/field regexes now also tolerate a
calling-convention macro before the `*` (`(ZEND_FASTCALL *name)`), which
hardens the existing struct-field path too.

Validated on two independent lineages: SameBoy (GB emulator) — 147 edges
via `opcodes[]`, 0 cross-file leak; php-src (Zend) — 54 edges across 7
tables in the designated+cast+CC-typedef form. Control: lua 0 — its
`lua_CFunction searchers[]` is pushed into the VM, never C-dispatched, so
the call-gate fires nothing. No regression on the #991 corpus: redis
(835) / sqlite (683) struct edges byte-identical, git +3 / curl +20
legitimate new bare-array edges, vim 433 with all guards holding; 0
non-function targets across all. + 4 fixtures.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:53:19 -05:00
dfe13b03c8 feat(mcp): off-load read-tool dispatch to a worker pool to fix concurrent-call timeouts (#1002)
The shared daemon served every session on one event loop with synchronous
node:sqlite. codegraph_explore is CPU-bound work stitched together by microtask
awaits, so N concurrent explores keep the microtask queue continuously full and
starve the macrotask phases — timers AND socket I/O. The transport freezes: no
response can flush until the whole batch drains, so with ~10 subagents on a large
repo clients routinely time out (reported via X by @symbolic2020).

Move the heavy read-tool dispatch onto a worker-thread pool. Each worker holds
its own WAL read connection (verified: a worker reader sees the main writer's
committed catch-up/watcher writes); the single watcher/writer, the catch-up gate,
codegraph_status, and the staleness/worktree notices stay on the main thread.
Concurrent reads now run in true parallel up to core count and the main loop
stays free for the MCP transport, so responses flush incrementally instead of
all-at-once after the batch drains. Enabled for the shared daemon only; direct
(single-stdio-client) mode is unchanged.

- crash recovery: respawn + retry-once, with a circuit breaker that falls back
  to in-process dispatch if workers can't run on this platform
- graceful backstop: an overloaded pool returns success-shaped "busy, retry"
  guidance, never isError (so it can't teach the agent to abandon codegraph)
- pending-aware growth + capped concurrent cold-starts avoid a startup
  thundering herd (N simultaneous module-loads + DB opens could stall the loop)
- config: CODEGRAPH_QUERY_POOL_SIZE (default clamp(cores-1, 1, 16); 0 disables
  → in-process), CODEGRAPH_QUERY_BUSY_TIMEOUT_MS (default 45s)

10 concurrent explores on vscode (10.5k files): 31s → ~9s, staggered flush,
0 timeouts, byte-identical output; scales with cores (≈3.3× on 8, 1.8× on 2).
Full suite passes plus 10 new query-pool tests (fake-worker injection so the
scheduling logic is covered without spawning threads).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:42:18 -05:00
Colby McHenryandClaude Opus 4.8 ec0c62522d chore(release): bump version to 1.1.1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:52:21 -05:00
7c6417ef8f fix(mcp): prevent "Transport closed" from a stray daemon-socket error (#974) (#983)
The client-facing MCP proxy could exit with "Transport closed" when its
connection to the shared daemon hit a socket 'error' with no listener
attached — common on WSL2 /mnt (DrvFs), where AF_UNIX is flaky. The global
fatal handler turned that uncaughtException into process.exit(1), which the
MCP client saw as a bare transport close even though the index was healthy.

proxy.ts now keeps an 'error' listener on the daemon socket for its whole
life (and skips a socket destroyed in the connect window), so a stray error
degrades to the existing in-process fallback instead of crashing. daemon.ts
releases the lockfile it acquired when it fails to bind, so the next launch
doesn't spin on a stale lock (the duplicate serve --mcp pileup).

No default behavior change for anyone; WSL /mnt users who still hit trouble
can set CODEGRAPH_NO_DAEMON=1 to skip the shared daemon entirely. Validated
on macOS (unit + live serve probe) and Linux (Docker, --init): 64/64 across
the daemon/socket/lifecycle suites, incl. real AF_UNIX.

Closes #974

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:48:16 -05:00
1e48861cfb docs(changelog): move .gitignore-respect fix to [Unreleased] (#981)
PR #980 merged after v1.1.0 was already promoted and published, so the
squash-merge's 3-way merge auto-placed its CHANGELOG entry under the
released [1.1.0] section. Move it to [Unreleased] so the published 1.1.0
notes stay accurate and the next release (1.1.1) promotes the fix.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:48:03 -05:00
73bcc1afb4 fix(extraction): respect .gitignore by default for embedded-repo discovery (#970, #976) (#980)
#514 (v1.0.0) began walking into gitignored directories to discover and
index the git repos nested inside them. That broke users who rely on
.gitignore to exclude a directory: a gitignored folder of cloned
reference repos blew graphs up (one report went 10k to 500k edges, #976)
and stalled indexing on multi-gigabyte trees of clones (#970).

Respect .gitignore by default again. Discovering embedded repos inside a
gitignored directory is now opt-in via codegraph.json:

    { "includeIgnored": ["packages/", "services/"] }

The single choke point findIgnoredEmbeddedRepos now returns nothing
unless a gitignored dir matches the project's includeIgnored patterns,
and the matcher is threaded from the scan root through the full-index,
incremental-sync, and watcher-scope paths. Downstream ScopeIgnore and the
watcher are unchanged: they key off the discovered embedded roots, so
gating discovery fixes the indexer, sync, and watcher together. Untracked
embedded repos (#193) stay indexed by default.

This restores the super-repo-of-clones behavior (#622, #699) for the
people who want it, while making the default match what every other tool
(and CodeGraph's own git ls-files foundation) does: .gitignore excludes.

project-config.ts now parses codegraph.json once (loadParsedConfig) and
exposes loadIncludeIgnoredPatterns alongside the existing extension map.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:44:49 -05:00
Colby McHenryandClaude Opus 4.8 ec42aa76b0 chore(release): bump version to 1.1.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:43:38 -05:00
85a8f32fd9 fix(mcp): serve tools without a root index + make the front-load hook monorepo-aware (#964) (#966)
The MCP server gated tool availability on whether the server root had a
.codegraph/ index, so in a monorepo where only sub-projects are indexed the
agent saw zero tools — and couldn't reach an indexed sub-project even by
projectPath. A session started before `codegraph init` also never surfaced the
tools afterward. The Claude front-load hook had the mirror gap: it only walked
UP for an index, so it stayed silent at a monorepo root.

MCP server:
- Always expose the tool surface; when the root isn't indexed, send a
  per-project instructions variant (pass projectPath) instead of the
  "inactive" note. Safety comes from response SHAPE (success-shaped guidance,
  never isError), not from hiding tools.
- Reword the no-default-project guidance to be per-project, not per-session,
  and sharpen the projectPath schema description.

Front-load hook (UserPromptSubmit):
- Scan DOWN (bounded depth, workspace-root-gated) for indexed sub-projects and
  shape the injection by topology: front-load the one the prompt names, nudge
  about the rest, or list them when ambiguous.

Verified: full suite (1703 passed); a live two-package monorepo run confirms the
hook front-loads the correct sub-project with no cross-package leakage. The
front-load's net speed effect is the existing multi-file-vs-single-file
tradeoff, unchanged by this work.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:57:47 -05:00
0a91d0f512 perf(resolution): fix O(K²) import-node blowup in "Resolving refs" (#915) (#965)
* perf(resolution): resolve imports to definitions, not sibling import nodes (#915)

"Resolving refs" crawled (tens of minutes) on large projects — most painfully
ones mixing a big front-end and back-end. An external package or module imported
across hundreds/thousands of files (react, a shared UI package, Python
logging/typing) is re-declared as an `import` node in every importing file, so
its unresolved import ref fell through to the exact-name matcher, which scored
all K same-named import nodes via findBestMatch — K refs x K candidates = O(K^2)
per package, producing only meaningless import->import edges.

Fix: exclude `import`-kind nodes as name-match targets (they're statements, not
definitions; real import->definition resolution is the import resolver's job).
Plus two safe constant-factor wins in findBestMatch: hoist the per-candidate
ref.filePath split, and skip cross-language candidates when a same-language one
exists (provably the same winner — same-language scores >=50, cross-language
maxes at 35).

Measured: superset (Py+TS) candidates scored 7.5M -> 833K (9x), non-import edges
preserved (+1618 now resolve to real defs), ~22K useless import->import edges
removed; kubernetes (Go) computePathProximity 37.2s -> 5.0s; synthetic 8k-file
mixed repo (K=4000) resolution 16.0s -> 1.7s. Full suite green.

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

* docs: correct stale better-sqlite3/wasm references to node:sqlite

The SQLite backend has been Node's built-in node:sqlite (real SQLite, WAL + FTS5,
from the bundled runtime) for a while — there is no native build step and no
node-sqlite3-wasm fallback. README and the docs site were already updated; this
catches the stragglers:

- CLAUDE.md: the src/db/ backend description and the sqlite-backend test note.
- src/db/index.ts, src/mcp/tools.ts: two code comments that still blamed "the
  wasm backend" for non-WAL behavior (reworded to "when WAL isn't in effect").

Leaves tree-sitter grammar wasm (web-tree-sitter / --liftoff-only) untouched —
that's a different, still-current use of wasm.

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

* chore(telemetry): drop the dead sqlite_backend field (schema v2)

node:sqlite is now the only backend, so the `index` event's `sqlite_backend`
field was a constant ("native") carrying no signal — and the `install` event
never actually sent it. Remove the field and the backendKind() helper, bump the
telemetry SCHEMA_VERSION 1 -> 2, and update TELEMETRY.md + docs/design/telemetry.md.

The ingest worker is deliberately left tolerant: `index` doesn't require the
field and schema_version validates as nonNegInt(99), so v2 events ingest fine and
old clients still sending v1 + sqlite_backend keep validating too. Added a legacy
comment there explaining it's safe to drop once old-client share is negligible.

telemetry.test.ts: the assertion pinning schema_version and a stale-claim fixture
line updated 1 -> 2. All telemetry tests pass.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:26:23 -05:00
a89315645d feat(go): index GoFrame g.Meta routes and bind them to controller methods (#747) (#957)
GoFrame's standard router binds routes reflectively (group.Bind(ctrl)): the path
and method live in a g.Meta struct tag on a request type, and the controller
method that serves it is matched by that request type at runtime — so there was
no path string and no edge from a route to its handler, and "where is this route
handled / where are routes bound to controllers?" could only be answered
lexically (issue #720's report).

- frameworks/goframe.ts: detect gogf/gf in go.mod, extract each path-bearing
  g.Meta into a route node (requires path:, so response mime:-only tags are
  skipped), encoding the package-qualified request type for the join.
- goframe-synthesizer.ts: join each route -> the controller method whose
  signature takes that request type — NOT by name (DeptSearchReq is served by
  List) — keyed pkg.Type to disambiguate the many identical bare names a large
  app defines one-per-module, with an addon-root tiebreak for cloned demo addons.
  Edge kind calls, provenance heuristic, synthesizedBy goframe-route, surfaced as
  a dynamic-dispatch hop in codegraph_explore.

Validated on real repos: gf-demo-user 7/7, gfast 65/68 (3 genuinely
handler-less), hotgo 242/247 (98%) — 100% precision (0 non-controller handlers,
0 core/addon cross-binding), node count stable. Agent A/B (gfast, sonnet/high,
2 runs/arm): with codegraph 1 explore call / 0 Read / ~20s vs without 7.5 Read
avg + grep-hunting for the non-existent literal route string / ~42s; same correct
answer.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:16:11 -05:00
6459ead6aa fix(extraction): index files reached through in-root symlinks that point outside the repo (#935) (#956)
The directory walk deliberately follows an in-root symlink whose target
lives outside the repo root (the standard Dota custom-game layout, where
`game/` and `content/` link into the SDK tree) and enumerates the files
under it. But the read path then rejected every one of them via the
strict symlink-escape guard, logging `Path traversal blocked in batch
reader` and indexing nothing — discovery and the reader disagreed.

Add an opt-in `allowSymlinkEscape` to validatePathWithinRoot that waives
only the realpath-escape rejection (the lexical `../` guard still
applies) and pass it at the three indexing read sites (batch reader,
indexFile, indexFileWithContent). The content-serving sinks
(ContextBuilder, MCP tools) keep the strict guard, so this stays inside
the #527 model: indexing now follows the symlink, getCode still refuses
to serve out-of-root contents.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:23:17 -05:00
d1121e46f0 feat(config): map custom file extensions to languages via codegraph.json (#906) (#955)
The extension → language table was hardcoded, so a codebase using a
non-standard extension for a supported language (e.g. `.dota_lua` for Lua)
had those files silently skipped — no way to opt them in short of patching
the source.

Add an opt-in, project-scoped `codegraph.json` at the repo root:

    { "extensions": { ".dota_lua": "lua", ".tpl": "php" } }

Mappings merge on top of the built-in defaults and take precedence (so a
built-in can be re-pointed, e.g. `.h` → `cpp`). Absent or malformed config
is the zero-config default — byte-identical to prior behavior; an invalid
target language or unparseable file is warned-and-skipped, never fatal.

Implementation:
- New `src/project-config.ts` — `loadExtensionOverrides(rootDir)`, validated
  against `isLanguageSupported`, mtime-cached per root.
- `detectLanguage` / `isSourceFile` gain an optional `overrides` arg
  (omitting it is the existing behavior).
- Overrides threaded per-operation through every extraction call site
  (scan/walk gates, git change-detection, grammar selection, extraction,
  the file watcher), resolved from the project root — no process-global
  state, so the multi-project daemon stays isolated. The parse worker
  receives the resolved language in its message.

Tests: 13 new cases (unit, loader validation/normalization/caching, and a
full-index integration proving a custom-extension file is extracted while
the zero-config path indexes nothing). Worker path smoke-tested via the
built CLI.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:16:23 -05:00
ba209d9489 feat(c/c++): resolve function-pointer dispatch (#932) (#954)
C/C++ polymorphism is the function pointer: a struct fn-pointer field, concrete
functions registered into it through a table (`{"add", cmd_add}`), a designated
initializer (`.handler = on_open`), or an assignment, then dispatched indirectly
(`p->fn(argv)`). Static extraction captures neither the registration→field
binding nor the indirect call, so the dispatcher→handler edge was missing — git's
run_builtin looked like it called nothing, a vtable's implementations had no
callers, and the hook_demo.c in the issue was unreachable.

Add a resolution-layer synthesizer keyed by (struct type, fn-pointer field). It
reads source (the established Celery/Sidekiq/Spring pattern — C extraction has no
struct fields or indirect-call edges to build on) in passes: collect fn-pointer
typedefs, parse struct field layouts, collect registrations (positional matched
by field index, designated, and assignment), propagate field←field assignments
(so a generic hook slot reassigned from a registry — the hook_demo.c
`h->func = found->fn` shape — inherits the registry field's handlers), then link
each indirect dispatch site to the registered handlers. Receiver type resolves
from the enclosing function's params/locals, falling back to a field name unique
to one struct. Covers both the command-table idiom (git, redis) and the
ops-struct/vtable idiom (curl content-encoders, protocol handlers).

Pure edge synthesis (no node growth); high precision via the (struct, field) key.

Validated: git 502 edges (run_builtin→cmd_* plus git_hash_algo/archiver/reftable
vtables), redis 357 (dictType.hashFunction, connection + reply-object vtables),
curl 478 (Curl_cwtype.do_init → deflate/gzip/brotli/zstd); 0 non-function targets
on all three; node-stable; 0 on the lua control (its {name,fn} tables register
into the Lua VM, with no C indirect call to bridge). Full suite 1665 pass.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:34:11 -05:00
826810f128 feat(java): index Lombok-generated members so call chains resolve (#912) (#953)
Lombok generates getters/setters, builder(), equals/hashCode/toString, and
the @Slf4j log field at compile time, so they never appear in the source AST.
Static extraction missed them entirely, so a bean.getName() / User.builder() /
log.info() call resolved to nothing and call-chain analysis broke silently —
the agent would conclude the method didn't exist.

Add a synthesizeMembers hook on LanguageExtractor, called at the end of class
extraction (class still on the scope stack, real members already extracted), and
a Java implementation that synthesizes the mechanical members for @Getter,
@Setter, @Data, @Value, @Builder/@SuperBuilder, @ToString, @EqualsAndHashCode,
and the @Log* family. Each node is anchored on the field/class name-token leaf
(so it pulls in no spurious value-reference scope), marked with a `lombok`
decorator and a docstring naming the generating annotation, and never overrides
a member the source already declares. Methods and fields are deduped separately
since they're distinct namespaces in Java (a boolean field `isRunning` and its
generated getter `isRunning()` coexist).

Deliberately not synthesized: constructors (new X() already links via
instantiates, and overloaded @NoArgs/@AllArgs/@RequiredArgs ctors would collide
on a synthetic node id), fluent builder setters, and @Accessors(fluent=true).

Validated on eladmin (274 Java files, Lombok-heavy): 100% accessor precision
(878/878 map to a real field), 722 previously-broken calls now resolve;
spring-petclinic (no Lombok) control synthesizes nothing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:41:26 -05:00
3e1547bbe1 fix(mcp): bold labels instead of ATX headings in tool results (#778) (#951)
MCP tool results used Markdown ATX headings (##/###/####) for section
headers — the status summary, each search hit, every file section in an
exploration — which Markdown-rendering clients (e.g. the Claude Code
VSCode extension) blow up to H1–H4 font size, filling the transcript with
oversized lines (worst on search/explore, where the noise scales with
result count). Swap them all for bold labels, which render at body size
while keeping the same structure. CLI/TTY output (ContextBuilder) is
unchanged — the issue notes it's fine.

The format is parse-coupled, so kept in sync:
- The explore truncation boundary and the offload chunker
  (reasoning/reasoner.ts) both key off the per-file header, now a unique
  `**`-prefixed marker emitted via a shared fileSectionHeader() helper.
- Updated the offload strip regexes and switched the opt-in report-style
  prompt off ATX headings (same client, same rendering issue).
- Updated test helpers (sectionFor, sourcedFiles, the callers
  section-boundary scan) that scanned the old markers.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:06:35 -05:00
ace8d8a0d0 fix(mcp): stop the first tool call hanging on a huge-repo catch-up reconcile (#905) (#950)
On a very large repo (the report is a ~93k-file / 5.7GB-DB Java monorepo) the
first MCP `tools/call` after a fresh `serve --mcp` could hang for 10+ minutes
with zero output, and with the liveness watchdog on, the daemon was SIGKILLed
mid-query instead. Root cause: the post-open catch-up reconcile that the first
tool call is gated on does ~2*N synchronous `fs.existsSync`/`fs.statSync` calls
plus a load-all-files query in two non-yielding loops. On a huge repo that wedges
the event loop for minutes, which (a) trips the 60s watchdog (it SIGKILLs a
process whose loop stops turning) and (b) blocks the first call the whole time.

Two complementary fixes:

- Make the reconcile yield. `ExtractionOrchestrator.sync()` now uses the
  yielding `scanDirectoryAsync`, and both O(files) reconcile loops
  `await setImmediate` every SYNC_RECONCILE_YIELD_INTERVAL (1000) files. The loop
  can no longer wedge the main thread, so the watchdog stays fed and the socket /
  any concurrent read stays responsive while a big reconcile runs. Results are
  unchanged — only yield points are added.

- Time-box the catch-up gate. The first `tools/call` now waits on the reconcile
  for at most CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS (default 3000ms), then serves and
  lets the reconcile finish in the background (which now yields, so the served
  call runs concurrently). `=0` restores the old unbounded wait. On a normal repo
  the reconcile finishes well under the budget, so behavior is unchanged.

Tests: adds two time-box cases to mcp-catchup-gate (serves promptly when the
reconcile runs long; `=0` restores the unbounded wait). Full suite green
(1655 passed). Validated end-to-end through the real daemon: first call returns
at the ~3s time-box instead of waiting an injected 8s reconcile; no-delay control
unchanged; `=0` opt-out waits the full reconcile.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:31:49 -05:00
2010c2d2b5 fix(sync): apply the ignore matcher to the git change-detection fast path (#766) (#949)
Change detection's git fast path (collectGitStatus) consumed `git status`
output with only an isSourceFile filter, on the assumption that git already
omits ignored paths. It doesn't: gitignore is a no-op for *tracked* files, and
the built-in default excludes (vendor/, node_modules/) aren't gitignore at all.
So a tracked file inside a committed dependency dir, or under a .gitignored
dir, surfaced as a change the full index never tracks — `codegraph status`
reported phantom pending changes that `sync` (a filtered filesystem reconcile)
never cleared, and the public getChangedFiles() API returned the same wrong
list.

Apply buildDefaultIgnore(repoDir) per recursion level, matching repo-relative
paths — structurally equivalent to the full-index path's ScopeIgnore (each
embedded repo judged by its own rules) with no extra git subprocess calls.
Deletions stay unfiltered: getChangedFiles acts on one only when the path is
already tracked in the DB, where removal is always correct, and that lets a
newly-excluded dir's stale rows clean themselves up.

Unblocks #699 (an .ignore overlay inherits this leak unless change detection
consults the same matcher as enumeration).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:51:20 -05:00
Colby MchenryandGitHub 149b4e11c7 Enhance README with CodeGraph benefits and image
Added an image and a note on cost savings for CodeGraph.
2026-06-22 10:30:57 -05:00
Colby McHenry 2f3188eb49 docs: reframe value prop around precision/speed, update Node floor to 20, and expand language/framework coverage
- Benchmark table reordered to lead with tool calls, time, and file reads (the universal wins); cost and tokens moved right with a note that savings are scale-dependent, not a headline claim
- README/introduction/quickstart/installation messaging updated to "surgical context · fewer tool calls · faster answers" framing, dropping the "16% cheaper" headline
- Node engine floor raised from 18 to 20 in CLAUDE.md, package.json description updated
- `codegraph init` now creates and indexes in one step; the `-i` flag is retired (still accepted as a no-op)
- CLI reference expanded with new commands: `explore`, `node`, `unlock`, `daemon`, `telemetry`, `upgrade`, `version`, `help`
- MCP server docs clarified: single `codegraph_explore` tool exposed by default, others unlisted but re-enableable via `CODEGRAPH_MCP_TOOLS`
- Language support adds Objective-C, Astro, and R; framework routes adds Play, Vue Router/Nuxt, and Astro
- API reference documents lower-level exports and embedding requirements (Node 22.5+ for `node:sqlite`)
- Troubleshooting adds WSL/Windows dual-checkout guidance
- How-it-works updated: SQLite backend is now Node's built-in `node:sqlite` in WAL mode, not better-sqlite3/WASM
2026-06-22 09:45:57 -05:00
f63e5db2cc fix(extraction): drop the phantom C++ function from a macro-annotated class misparse (#946) (#948)
A C++ class/struct annotated with an export/visibility macro —
`class MYLIB_EXPORT Foo : public Bar { … }` — makes tree-sitter read
`class MYLIB_EXPORT` as an elaborated type specifier and the whole declaration
as a `function_definition` named after the class, spanning the entire body. That
phantom `function` polluted callers/impact/blast-radius and skewed kind stats.

Detect the misparse structurally in cppExtractor.isMisparsedFunction — a
function_definition whose `type` field is a *bodyless* class/struct specifier
(the elaborated-type macro) and whose declarator is not a function_declarator —
and drop the bogus node, matching how macro-prefixed C prototypes are already
handled. The body is mangled by the same misparse and is unrecoverable. Precise
enough to leave genuine code alone: `struct P { int x; } makeP() {}` (real
inline-defined return type, has a field list) and `class Foo f() {}` (elaborated
return type on a real function, has a function_declarator) are untouched. The
leading macro alone triggers the misparse; a base clause is not required.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:45:50 -05:00
2bdc169ce4 fix(extraction): skip submodule worktrees instead of indexing them as duplicates (#945) (#947)
A worktree of a submodule points its `.git` into
`.git/modules/<module>/worktrees/<name>`, but `classifyGitDir` only matched
the top-level `.git/worktrees/` shape — so submodule worktrees fell through
to "embedded" and every symbol they shared with the real submodule checkout
got indexed twice (one report: ~28% of the index was duplicates, inflating
both query results and the DB). Broaden the worktree detector to allow the
optional `modules/<module>` segment. The submodule's own checkout
(`.git/modules/<module>`, no `worktrees/`) is unaffected and stays indexed as
distinct code.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:44:06 -05:00
03666584ed fix(extraction): drop the ./ self-entry from git ls-files --directory (#936) (#941)
When the indexed root is a directory an enclosing git repo ignores,
`git ls-files --directory` collapses the whole cwd to a single literal
`./` entry. That sentinel reached the `ignore` matcher, which rejects it
("path should be a `path.relative()`d string, but got "./""), aborting
buildScopeIgnore — the one ignore-building call in FileWatcher.start().
So the MCP daemon's startWatching() threw, was caught as "Failed to open
project", and auto-sync never started: the index silently went stale
until a manual `codegraph sync` (CODEGRAPH_NO_DAEMON=1 was the only
workaround).

Filter the `./`/`.` self-entry wherever we consume `--directory` output
(listIgnoredDirs + the untracked-dir loop in discoverEmbeddedRepoRoots).
Semantically correct, not just a crash guard: `./` means "the whole cwd",
never a nested repo to recurse into.

Not platform-specific (reported on Codex/Windows, reproduced on macOS):
the trigger is git state, not the OS.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:13:23 -05:00
e43ac82cdf fix(mcp): reopen the database when it's replaced on disk instead of serving a deleted inode (#925) (#940)
A long-lived `serve --mcp` process opens `.codegraph/codegraph.db` and holds
the fd for its whole life. If `.codegraph/` is removed and recreated AT THE
SAME PATH while it runs — `git worktree remove <p>` + re-add, or `rm -rf
.codegraph` + `codegraph init` — the held fd points at the now-unlinked inode
and can never see the new index. The server serves the pre-removal snapshot
(renamed/removed symbols still "live", new ones missing); `codegraph sync`
can't refresh it and the CLI (a fresh process) diverges. Only a restart fixed
it — and because the daemon registry is keyed by path, a same-path recreate
routes new clients straight back to the same stale daemon, so the fix has to
self-heal inside the running process.

- DatabaseConnection records the DB file's (dev, ino) at open and exposes
  isReplacedOnDisk() — a different inode now at the same path. POSIX-gated:
  Windows can't unlink an open file and its st_ino is unreliable, so it never
  fires there.
- CodeGraph.reopenIfReplaced() opens the live file first, then swaps the
  connection + query layers IN PLACE (via the new wireLayers() helper), so
  every holder of the instance (the daemon's default project, cached
  projectPath connections) heals without a restart. Closing the dead handle
  also frees the leaked db/-wal/-shm fds pinning the unlinked inode.
- ToolHandler.getCodeGraph calls it (freshen) before serving — one stat() per
  call, a no-op unless the inode actually changed, never throws into a tool.

Tests cover isReplacedOnDisk (unchanged / replaced / absent / Windows-gated)
and an end-to-end reopen that heals a held instance after a same-path recreate
(asserts the pre-heal staleness too). Validated on macOS with a dist probe of
the raw instance and the MCP serving path; full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:03:07 -05:00
62d5cdde2c fix(mcp): re-resolve a path's index every call so worktree state isn't pinned (#926) (#939)
A long-lived MCP server (the shared daemon) cached both the project handle
(projectCache) and the worktree-mismatch verdict (worktreeMismatchCache) for
its whole lifetime — cleared only on shutdown — keyed off the input path and
never re-checked. So a worktree path first resolved BEFORE it had its own
.codegraph/ — when the walk-up reached the main checkout — stayed pinned to
the main checkout: every query kept hitting the wrong index and every result
carried a false "this index belongs to a different git working tree" warning,
until the server restarted. The CLI was correct (fresh process per run);
re-indexing didn't help.

- getCodeGraph: re-resolve findNearestCodeGraphRoot on every call (cheap stat
  walk, no git, no reopen) and cache the open DB by RESOLVED ROOT only. Drops
  the input-path short-circuit that pinned the first resolution, and the
  double-keying (which also double-closed each instance in closeAll()).
- worktreeMismatchFor: key the verdict on (startPath, indexRoot) so a changed
  index root recomputes instead of serving the stale "borrowed the parent's
  index" verdict.

Adds a regression test that fails pre-fix (the stale warning survives the
index-root flip); the existing "no further git spawn" caching test still
passes. The query-staleness half (a second project opened through the handler)
isn't unit-testable under vitest, so it was validated with a dist A/B.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:43:43 -05:00
Colby MchenryandGitHub b218f625f7 Merge pull request #937 from colbymchenry/codegraph-ai
Engine batch: dispatch-synthesizer family, React component/route coverage, installer UX + front-load hook
2026-06-21 12:59:21 -05:00
Colby McHenryandClaude Opus 4.8 bd4814d8c1 feat(installer): stop auto-indexing on install + ship opt-in front-load prompt hook
`codegraph install` no longer indexes the current directory — it wires up agents
only, and building a project's graph is always the explicit `codegraph init` /
`index`. Removes the global-vs-local inconsistency (a local install silently
indexed, a global one didn't) and the docs/behavior mismatch (#826). README
updated to match; the stale `init --index` note (indexing is default now) fixed.

Adds an opt-in Claude Code front-load hook: a `UserPromptSubmit` hook that runs
the new hidden `codegraph prompt-hook`, which injects codegraph_explore context
for structural ("how / where / trace / impact") prompts so the agent answers
from the graph instead of grepping to rebuild it. Prompted at install
(default-yes; Claude-only — the only agent with prompt hooks), removed on
uninstall, and `codegraph upgrade` self-heals it onto an already-configured
global Claude install. Strictly additive + degradable: non-structural prompts,
un-indexed projects, and any failure are silent no-ops. Disable without
uninstalling via CODEGRAPH_NO_PROMPT_HOOK=1.

7 new installer-targets contract tests (write / idempotent / opt-out round-trip /
sibling-preserved / uninstall / legacy-independent). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:36:41 -05:00
Colby McHenryandClaude Opus 4.8 212dfc4b6a docs(changelog): note the sync cross-file caller-edge fix (#899)
PR #927 (merged to main) fixed `sync()` dropping incoming cross-file
calls/references edges on callee re-index but did not add a CHANGELOG
entry; add it to [Unreleased] so it lands in the next release notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:50:24 -05:00
Colby McHenryandClaude Opus 4.8 64426cad93 fix(react): recognize forwardRef/memo/styled components + index JSX-file routes (#841)
forwardRef/memo/styled-wrapped component consts were classified as plain
`constant` nodes (the initializer is a call/tagged-template, not a bare arrow),
so the JSX-render synthesizer and component resolution skipped them — callers
and impact returned empty for the entire shadcn/ui-style UI layer. Recognize
them in the tree-sitter extractor as `component` nodes (correct body range +
callee capture), PascalCase-gated so a memoization util stays a constant.

Separately, the `react` resolver's `languages` lacked 'tsx'/'jsx', so its
`extract()` never ran on JSX files — React Router `<Route>`/createBrowserRouter
and Next.js page routes (which only live in .tsx/.jsx) were never indexed. Add
'tsx'/'jsx' and make `extract()` route-only: the component/hook regex it carried
duplicated tree-sitter nodes (a `useAuth` became two `function` nodes) and is
fully superseded by the extractor now.

Validated before/after: taxonomy 0->99 component nodes (35 w/ callers) + 1->15
routes; radix 0->262 components (80 w/ callers); cypress-realworld-app 45->52
routes (7 <Route> tags from .tsx); non-React control unchanged; node count
stable. New tests: react-hoc-component.test.ts + a route e2e in
frameworks-integration.test.ts.

Root-caused by @maxmilian (#846); reported by @Arlandaren.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:35:12 -05:00
Colby McHenryandClaude Opus 4.8 b5090cbad5 docs(dispatch-backlog): shelve trezor barrel-registry as single-lineage/overfit
Discovery across 15 independent diverse repos + GitHub-wide code search found
the strict barrel-namespace shape (`import * as M from './api'` -> `M[runtimeKey]`
-> `new` -> `.run()`) in exactly 2 repos: trezor-suite and OneKey hardware-js-sdk.
But OneKey is a @trezor/connect fork (same findMethod/MethodConstructor skeleton),
so it's 2 indexable repos but one design lineage = effectively n=1. Every
independent registry-by-runtime-key found is a different shape the trezor-tuned
synth wouldn't catch (n8n dynamic-import+DI, polkadot array-of-constructors,
ccxt object-literal [already covered], typeorm/xrpl switch). The synth is the
hard tier (cross-file barrel re-export enumeration + computed index + camel/Pascal
transform + entry-method fan-out) -- meaningful complexity for a single-lineage
win, which the overfit discipline says not to build. Feasibility was fine
(the import resolver already chases re-export barrels); the blocker is corpus
thinness. Reopen only if an independent (non-trezor-lineage) repo appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:19:02 -05:00
Colby McHenryandClaude Opus 4.8 feb2f641de feat(resolution): bridge Laravel event(new X) to its listener handles
Laravel decouples an event dispatch from its listener(s), linked by the event
class: event(new OrderShipped($order)) has no static edge to the
handle(OrderShipped $event) that runs it (usually a separate app/Listeners/
class). laravelEventEdges bridges each event(new X(...)) site -> every
listener's handle for X.

Two registration mechanisms, both real and both needed (built together):
- (A) auto-discovery: a typed handle(EventType $e) first param, read from the
  method declaration source (PHP method nodes carry no signature, like C#); a
  handle(A|B $e) union is split into two events.
- (B) the `protected $listen = [XEvent::class => [Listener::class, ...]]` map in
  an EventServiceProvider, parsed from comment-stripped source (so a
  fully-commented map on an auto-discovery app contributes nothing). This is the
  only way to link a listener whose handle() is untyped.

Job exclusion is free: queued jobs dispatch via ::dispatch()/dispatch() (not
matched) and their handle() takes an injected service, never an event type, so
matching only event(new X) excludes them by construction. `use Dispatchable` is
not keyed on (unreliable in real apps).

Surfaces as `dynamic: laravel event` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising both
mechanisms: koel (small, populated $listen map, 9 edges incl. the untyped-handle
case and a fan-out) and firefly-iii (large, pure auto-discovery / empty $listen,
141 edges, 0 source/target false positives, 0 namespace mismatch, union split
verified); 0 on the guzzle control. Namespace-agnostic (FireflyIII\ not
hardcoded). Node-stable (pure edge synth). Suite 1623 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:45:24 -05:00
Colby McHenryandClaude Opus 4.8 2c522c6254 feat(resolution): bridge Sidekiq Worker.perform_async to #perform
Sidekiq decouples a job's enqueue site from the worker's perform method,
linked by the worker class NAME: DestroyUserWorker.perform_async(id) has no
static edge to DestroyUserWorker#perform (usually in app/workers/, away from
the controller/model that enqueues it). sidekiqDispatchEdges bridges each
Worker.perform_async/_in/_at(...) site -> that worker's instance perform.

Name-keyed, like Celery: the receiver class must be a Sidekiq worker, gated by
reading `include Sidekiq::Job|Worker` from the class body (the mixin is an
external gem module that forms no resolvable edge). ActiveJob's perform_later/
_now is a different shape and deliberately not matched.

Namespace disambiguation was the n>1 validation payoff: loomio's flat workers
hid a collision bug that forem exposed (four SendEmailNotificationWorker classes
across modules; simple-name resolution mis-targeted 7/143 edges to the wrong
namespace). Fixed by resolving a namespaced receiver via exact qualified-name
lookup first, falling back to the simple name only for a unique worker — an
ambiguous unqualified collision bails (precision over recall).

Surfaces as `dynamic: sidekiq dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos: loomio (medium,
Sidekiq::Worker, 47 edges) and forem (large, both include aliases — 131
Sidekiq::Job + 11 Sidekiq::Worker, 142 edges, 0 worker/source false positives,
0 namespace mismatch); 0 on the jekyll control. Node-stable (pure edge synth).
Suite 1621 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 01:32:33 -05:00
Colby McHenryandClaude Opus 4.8 d1381e11f6 feat(resolution): bridge MediatR Send/Publish to its IRequestHandler.Handle
MediatR decouples a _mediator.Send(x)/.Publish(x) call from the Handle method
that runs it, linked by the request/notification TYPE (the IRequestHandler<X,…>
generic), usually across files in a Clean Architecture layout — so flows
dead-end at the mediator call and the agent reads to find the handler.
mediatrDispatchEdges bridges each dispatch -> the matching handler's Handle.

Same two-pass, type-keyed shape as the Spring synthesizer, with two C#-specific
twists found by probing:

- C# method nodes carry NO signature (csharp.ts defines no getSignature), so
  Pass 1 reads the request type from the handler CLASS base-list source
  (`: IRequestHandler<X,…>` first generic arg) and binds the class's Handle.
- The dominant .NET idiom is VARIABLE-passed, not inline `Send(new X)` — eShop
  has zero genuine inline MediatR sends. So Pass 2 resolves the sent type from
  the argument three ways within the enclosing method: inline `new X(…)`, a
  local `var v = new X(…)` (backward scan), or a parameter/local declared `X v`.

Two precision gates: the receiver must be mediator-ish (mediator/sender/
publisher — excludes MAUI MessagingCenter.Send, HttpClient.Send) AND the
resolved type must have a handler (so a same-named non-request DTO is never
bridged). Handles the IdentifiedCommand<T,R> wrapper and void IRequestHandler<T>.

Surfaces as `dynamic: mediatr dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos: jasontaylordev/
CleanArchitecture (small, 9 edges, inline + param forms) and dotnet/eShop
(medium, 9 edges, 0 false positives, variable-passed + IdentifiedCommand +
the CancelOrderCommand DTO-collision correctly avoided); 0 on the
Newtonsoft.Json control. Node-stable (pure edge synth). Suite 1619 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:34:24 -05:00
Colby McHenryandClaude Opus 4.8 8591ea5993 chore: gitignore docs/business/ (confidential, keep out of public repo)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:18:01 -05:00
Colby McHenryandClaude Opus 4.8 9b7ca2e394 feat(resolution): bridge Spring publishEvent() to its @EventListener handlers
Spring decouples an event publisher from its listener(s) through the
application event bus, linked by the event TYPE: publishEvent(new XEvent(...))
has no static edge to the @EventListener void on(XEvent e) that handles it
(usually a different class), so flows dead-end at the publish and the agent
reads to find the handlers. springEventEdges bridges each publishEvent(new X)
site -> every listener of X.

Two-pass, type-keyed (no name resolution, so precision is structural):
- Pass 1 builds Map<eventType, listenerMethod[]> from @EventListener /
  @TransactionalEventListener methods (event type = first param type off the
  node signature, or the @EventListener(X.class) value form) and the older
  `implements ApplicationListener<X>` onApplicationEvent methods.
- Pass 2 links each publishEvent(new XEvent(...))'s enclosing method to every
  listener of XEvent; multi-line `publishEvent(\n new X(...))` handled.

Key Java fact (probed): a method node's range INCLUDES its leading annotations
(startLine is the first @-line, not the `public void` decl), so the annotation
gate scans DOWNWARD from startLine bounded to consecutive @-lines, which can't
bleed into an adjacent method.

Surfaces as `dynamic: spring event` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising all listener
forms: halo (medium, 1254 java, 33 edges across 24 events, 0 publisher/listener
false positives, param-typed + (X.class) + ApplicationListener + fan-out) and
thombergs/code-examples (4 edges, adds @TransactionalEventListener); 0 on the
gson control (no Spring). Node-stable (pure edge synth). Suite 1617 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:00:43 -05:00
Colby McHenryandClaude Opus 4.8 6e5c3a9336 feat(resolution): bridge Celery .delay()/.apply_async() dispatch to the task body
Celery decouples a task's call site from its body: a @shared_task / @app.task
decorated def is invoked via task.delay(...) / task.apply_async(...), a dynamic
hop with no static edge, so flows dead-end at the dispatch and the agent reads
tasks.py to reconstruct them. celeryDispatchEdges links the enclosing function
at each .delay/.apply_async site -> the task function body.

Precision rests on a DECORATOR gate: the dispatched name must resolve to a
Python function carrying a task decorator, read from the source lines ABOVE its
def (the def's startLine excludes the decorator, and no decorates edge exists
since @shared_task is an unresolved external import). The kind==='function'
filter drops same-named test-method collisions; canvas forms (group(t).delay(),
t.s()/.si()) have no single identifier before .delay so they're skipped, not
mis-bridged; cross-module name collisions prefer a same-file task else bail.

Surfaces as `dynamic: celery dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising both decorator
dialects: paperless-ngx (small, @shared_task, 31 edges, 31/31 real) and pretix
(medium, @app.task, 63 edges across 21 tasks, 0/21 false positives); 0 on the
httpie control (no Celery). Node-stable (pure edge synth). Suite 1615 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:22:52 -05:00
Colby McHenryandClaude Opus 4.8 80a1044d3d feat(resolution): bridge Vuex string dispatch/commit to actions and mutations
Completes the Vue store dispatch family (the Pinia bridge was 8ea3205). Vuex
dispatches by a runtime STRING key — `dispatch('user/login')` /
`commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')` — with no
static edge to the handler.

vuexDispatchEdges (callback-synthesizer.ts): the last `/` segment of the key is
the action/mutation name, the preceding segment is the namespace (≈ the module
file). Resolve the name to a function node IN A STORE FILE — the ≥2-signal
store-file gate excludes a same-named `api/` helper (`getInfo`/`login` collide in
practice) — disambiguated by the immediate namespace segment appearing in the
path (handles deep nesting like `d2admin/user/set`), or the same file for a root
local `commit('M')` inside an action. The .vue component is a dispatcher fallback
for top-level setup calls. Surfaces in explore as `dynamic: vuex dispatch`.

Also extracts the canonical Vuex MODULE shape `export default { namespaced,
actions: {…}, mutations: {…} }` (tree-sitter.ts: extractStoreCollectionMethods
off the export_statement, store-file gated) — its object-literal methods were
otherwise never nodes, so d2-admin's actions couldn't be bridged.

Validated 100% precision on three repos — vue-element-admin (55 edges),
vue-admin-template (12), d2-admin (63): 0 non-store targets, 0 namespace
mismatches (54/54 namespaced edges route to the correct module despite 6
colliding `load` actions in d2-admin), 0 on Redux controls (basetool/uwave —
non-string `dispatch()` correctly ignored). Suite green (1613); new
__tests__/vuex-dispatch-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:49:06 -05:00
Colby McHenryandClaude Opus 4.8 8ea32059b6 feat(resolution): bridge Pinia useStore().action() calls to the action
The dispatch bridge for Pinia, on top of the store-action extraction foundation
(cc9c2f7). A consumer does `const store = useXStore()` then `store.action()` —
a method-on-instance call with no static edge to the action, which lives in the
store module. So tracing "what does this view do when it loads" stopped at the
`store.fetchUser()` line.

piniaStoreEdges (callback-synthesizer.ts): map each `const useXStore =
defineStore(...)` factory → its file; per consumer file, bind `const s =
useXStore()` vars; link the enclosing function (or the .vue component, via a
fallback) → the `s.method()` action node IN THE STORE'S FILE. The same-store-file
gate is the precision lever — a Pinia built-in (`$patch`) or an unrelated
same-named method resolves to nothing. Covers the options and setup store forms
uniformly (the action is a function node in the store file either way) and
surfaces in explore as `dynamic: pinia store`.

Validated 100% precision (Geeker 41 edges, MallChat 64; 0 targets outside a
store file), 0 on the Vuex-only element-admin control (no defineStore), n=2 in
hand. Suite green (1612); new __tests__/pinia-store-synthesizer.test.ts. The
Vuex string-key dispatch bridge (`dispatch('ns/action')`) remains a follow-up
(n=1 in hand — needs a 2nd string-literal Vuex repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:30:13 -05:00
Colby McHenryandClaude Opus 4.8 cc9c2f7420 feat(extraction): index Vuex/Pinia store actions, mutations, and getters
A Vue store's callable surface — Vuex `actions`/`mutations`/`getters` and Pinia
store actions — lived only as object-literal properties, so the symbols an agent
looks for (`login`, `getSessionList`, `getAuthMenuList`) were never nodes:
`codegraph search`/`codegraph_node` returned "not found" and the agent had to
read the store by hand. This extracts them as function nodes (with their real
bodies + callees), the foundation under any later dispatch-bridge synthesis.

A corpus probe (vue-element-admin, vue2-elm, Geeker-Admin, MallChatWeb) showed
Vue store dispatch is NOT one clean string-keyed shape but ~5; extraction here
covers the three dominant definition forms:
  - Vuex MODULE: non-exported `const actions/mutations = {…}` collections
    (gated by a ≥2-signal looksLikeVueStoreFile + the object-of-functions shape,
    so a Redux file's stray `const actions` is a 0-node no-op).
  - Pinia OPTIONS: `defineStore({ actions: {…}, getters: {…} })` — methods of
    the actions/mutations/getters properties of a store-factory config.
  - Pinia SETUP: `defineStore('id', () => { const foo = …; return {…} })` — the
    body-local function consts (findPiniaSetupFn + extractPiniaSetupBody; the
    generic body walk doesn't reach nested function scopes). Distinguished from
    an inline action map via objectHasInlineFunctions so zustand/SvelteKit
    extraction is unchanged.

Validated findable on element-admin (50 fns), Geeker (21), MallChat (68);
0-node no-op on a non-Vue control (uwave-web, unchanged at 4496 nodes). Deferred
(documented in the backlog): vue2-elm's `export default {…}` split-file +
computed-key `commit(CONST)` form (n=1), and the dispatch BRIDGE synthesis
(Vuex string-key + Pinia useStore().action()). Suite green (1610); new
__tests__/vue-store-extraction.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:08:34 -05:00
Colby McHenryandClaude Opus 4.8 e9f7422223 feat(resolution): synthesize RTK Query hook→endpoint dispatch edges
Adds the RTK Query member of the dispatch-through-indirection family
(synthesizedBy:'rtk-query'). An RTK Query endpoint defined inside
`createApi({ endpoints })` and the `useGetXQuery`/`useUpdateYMutation` hook it
generates were both invisible to static extraction, so a `component →
useGetXQuery → getX → queryFn` flow had nothing to connect and explore
dead-ended on the API slice.

Extraction (tree-sitter.ts): mint a function node per endpoint — named by its
key, spanning the queryFn/query handler so its calls attribute — handling both
the `endpoints: build => ({...})` arrow and `endpoints(builder){ return {...} }`
method forms, with a bare-node fallback for factory handlers
(`queryFn: makeFn(url)`); and a function node per generated-hook binding from
`export const {...} = api`, carrying a sentinel signature.

Resolution (callback-synthesizer.ts): rtkQueryEdges bridges each generated-hook
node to its same-file endpoint by the naming convention (strip use + optional
Lazy + Query|Mutation, lowercase head). Component→hook is normal import/call
resolution; the hook→endpoint hop surfaces in explore as `dynamic: rtk query`.

Validated 100% precision (hooks == synth edges, 0 cross-file) on basetool (54),
minusx-metabase (11), shapeshift (13); 0 on the uwave-web control (no createApi
→ a complete no-op). The sentinel gate correctly ignores hand-written
look-alikes (shapeshift's useFoxyQuery is a real custom hook, never bridged).
Full suite green (1608); new __tests__/rtk-query-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:23:37 -05:00
Colby McHenryandClaude Opus 4.8 7f970296cf feat(resolution): synthesize object-literal registry dispatch edges
Adds `objectRegistryEdges` — a dynamic-dispatch synthesizer for the command/handler
registry pattern: an object literal maps string keys → handler classes/functions, then
dispatches by a RUNTIME key static parsing can't follow:

    this.commands = { [Cmd.ADD]: AddObjectCommand, ... }    // registration
    new this.commands[command](args).execute()              // dynamic dispatch

It links each dispatching function → each registered handler's callable entry (a class's
execute/run/handle method — preferring the method chained at the dispatch site — or the
function value), like the gin-middleware-chain fan-out. Same-file registry+dispatch only.

Validated precise on 3 real repos (the discipline that caught redux-thunk's n=1 overfit):
EtherealEngine's CommandManager (64 edges, class registry → .execute), Prebid.js (7:
builder/consent/message dispatch, function registry), warp-drive (1). Zero false positives
after several precision gates found during validation:
- skip minified/generated bundles (avg line length > 200) — draco/three.min were a
  false-positive minefield of `h[x](...)` calls + `{a:b}` literals;
- DEPTH-AWARE entry parsing (top-level `key: Identifier` only) so method-shorthand bodies
  and nested objects don't leak their inner `k: v` pairs as bogus handlers;
- callable-only targets (drop data `constant`s — a `{x: URL}` entry resolving to the global);
- dynamic-dispatch gate (a statically-accessed look-alike object yields nothing).
Handles constructor and field-initializer registry forms (this. normalized). Surfaces in
codegraph_explore via the existing Dynamic-dispatch-links section.

Deferred (recall, documented in dispatch-synthesizer-backlog.md): assign-then-call dispatch,
augmentation registration (reg[k]=H), and the cross-file barrel-namespace variant
(trezor getMethod) — the hard tier.

Full suite green (1606); new __tests__/object-registry-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:17:31 -05:00
Colby McHenryandClaude Opus 4.8 270e50655a fix(explore): surface synth constant-endpoint edges + precise redux-thunk dispatch resolution
Two fixes hardening the redux-thunk dynamic-dispatch synthesizer, found by
validating it on real RTK repos beyond its trezor origin (uwave-web,
session-desktop, octo-call):

- Surfacing: buildFlowFromNamedSymbols filtered its named set to CALLABLE
  kinds, so synthesized edges between `constant` nodes (RTK thunks are
  `const X = createAsyncThunk(...)`) never entered the Flow / Dynamic-dispatch
  links scan — invisible at every tier, while the kind-agnostic Relationships
  section is off below 500 files. Add a `dynNamed` set (named constant/variable/
  field nodes with a heuristic edge) feeding a shared collectSynthLinks into the
  "## Dynamic-dispatch links" section, threaded through the named.size<2
  early-out (both-endpoints-constant hit return EMPTY first) and the main path.
  Main call-chain stays callable-only; the <500 budget tiers are untouched.
  No-op for callable flows. Plus a generic synthEdgeNote fallback so any synth
  hop reads "dynamic: <kind> @site", not a bare "[calls]".

- Precision: reduxThunkEdges resolved a dispatched name by first-match-by-kind,
  so a thunk name colliding with a same-named service function linked to the
  wrong node (octo-call `leaveCall`). Prefer thunk-signature const > other
  const > same-file callable > first match.

Tests: new explore-synth-constant-endpoints.test.ts (surfacing on a small repo)
+ a collision case in redux-thunk-synthesizer.test.ts. Full suite green (1605).
Rationale + coverage backlog in docs/design/dispatch-synthesizer-backlog.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:45:51 -05:00
Colby McHenry e5897d0334 feat: remove reasoning offload / CodeGraph AI managed reasoning feature
Strips the bring-your-own-model reasoning offload and managed CodeGraph AI
integration (login/logout/usage commands, offload config/credentials/reasoner
modules, and the synthesizeOffload call in codegraph_explore). The eval findings
showed raw source output outperformed the synthesized path on accuracy, so
codegraph_explore reverts to returning verbatim retrieved source exclusively.

CHANGELOG and README sections for reasoning offload are removed; test comments
and DEFAULT_MCP_TOOLS description are updated to drop offload references.
2026-06-20 13:23:16 -05:00
Colby McHenry e7d9f8c6fa feat(explore): surface interface/registry dispatch boundaries and window oversize spine methods
Two gaps closed in `codegraph_explore` output quality:

**Interface/registry dispatch (#687 extension).** When a named token resolves to
a large same-name family (≥8 members) that doesn't land on the connected flow, the
static path truly ends there — the target is chosen at runtime from N implementations
(plugin/strategy/handler interface). `buildPolymorphicBoundaries` detects this via
`implements`/`extends` edges, ranks candidate supertypes by their TRUE graph-wide
implementer count (not FTS sample frequency, which is biased), and emits a
"## Interface dispatch" section naming the supertype, implementer count, and a few
concrete targets. Fires only for uncovered named tokens; a connected flow stays silent.

**Oversize spine method windowing.** A flow entry that is a god-method (e.g. n8n's
962-line `processRunExecutionData`) previously lost the per-file budget to denser
peripheral blocks and was dropped, forcing the agent to `Read` it back. The spine
call site (edge line to the next hop) is now tracked via `spineCallSites` and used
to window the method to its signature head + a ±28-line band around the call, keeping
it under the OVERSIZE_SPINE_LINES threshold. Spine clusters also rank first in the
budget sort and may exceed the per-file cap up to a 2.5× ceiling so they can never be
starved by co-flow files.

Test suite gains an `interface dispatch` describe block (announce, silent-on-connected,
silent-below-threshold) and uses `beforeAll`/`afterAll` to pin `CODEGRAPH_OFFLOAD_DISABLE=1`
so structural assertions are hermetic regardless of machine config.
2026-06-20 12:32:05 -05:00
Colby McHenry 4f8782cbe5 test(agent-eval): add output-style A/B harness, cost/token analyzer, and DISALLOW/REP_START controls
Three additions to tighten the eval loop:

- offload-eval-styles.sh: new 4-arm eval (raw/refs/map/src) isolating the Worker's
  output shape's effect on main-session tokens, latency, and accuracy. Delegation
  blocked by default (DISALLOW=Agent) so variance from Haiku subagent spawning doesn't
  contaminate the measurement.
- offload-eval-cost.mjs: cost/token analyzer that reads Claude Code's own per-model
  accounting (modelUsage.costUSD) rather than re-deriving from raw token counts,
  giving a correct main(Sonnet)/sub(Haiku) split with proper per-tier pricing.
- offload-eval-3arm.sh: adds DISALLOW env to block sub-agent delegation across all
  arms, and REP_START to append reps to an existing run without clobbering earlier
  jsonls (e.g. REP_START=4 REPS=3 → reps 4,5,6).

Also adds CODEGRAPH_OFFLOAD_STYLE forwarding to the managed gateway so the styles
eval can drive output shape end-to-end; the field is stripped before the upstream
model call and never sent to BYO endpoints.
2026-06-19 16:43:10 -05:00
Colby McHenry 4aa2752ef1 chore: stop tracking .claude/handoffs (local session notes only) 2026-06-19 02:17:43 -05:00
Colby McHenry 291b200ece chore: stop tracking .claude/handoffs (local session notes only) 2026-06-19 02:16:50 -05:00
Colby McHenry f82a662ddb feat(mcp): pare default tool surface to codegraph_explore alone + redux-thunk synthesizer 2026-06-19 02:15:14 -05:00
Colby McHenryandClaude Opus 4.8 7ddd3fa7eb test(agent-eval): persist offload accuracy/adoption eval harness + front-load hook
Reproducible suite measuring the managed CodeGraph AI offload and the front-load
UserPromptSubmit hook (approach 1) vs raw codegraph and no-codegraph, across repo
sizes, on time / main-session tokens+cost / CodeGraph-AI tokens+cost / accuracy.
All agent arms run claude -p sonnet --effort high; eval-only, nothing shipped.

- offload-eval-setup.sh: clone + index 4 memory-probe-verified "not-trained-on" repos
  (mtkruto/postybirb/shapeshift/trezor — small→large) so the no-codegraph baseline is honest.
- offload-eval-3arm.sh / -frontload.sh: one repo, the arms (offload/raw/nocg, frontload).
- offload-eval-matrix.sh / -frontload-matrix.sh: drive all 4 tiers.
- offload-eval-hook.mjs: the front-load hook (self-locates its engine; CG_FRONTLOAD_DEBUG to log).
- offload-eval-metrics.mjs / -judge.mjs (Sonnet) / -summarize.mjs: extract, score, aggregate.
- offload-eval-ground-truth.json: source-verified canonical flows (the judge's reference).
- offload-eval.md: usage + the 2026-06 findings (raw = the win; offload least-accurate;
  front-load solves adoption but exposes explore's dynamic-dispatch gaps).

Scripts are path-portable (self-locating $HERE/$ENGINE; AGENT_EVAL_OUT scratch dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:30:30 -05:00
Colby McHenry 6d5cb6b25c feat(reasoning): add CODEGRAPH_OFFLOAD_DISABLE kill-switch and per-call usage log
`CODEGRAPH_OFFLOAD_DISABLE=1` immediately disables the offload for the current
process without touching the persisted config or stored login — useful for A/B
arms or sessions where raw source is preferred.

`CODEGRAPH_OFFLOAD_USAGE_LOG=` appends one JSONL entry per call with token
counts, charged credits, and derived cost (`creditsCharged / 100_000`) so a
harness can attribute CodeGraph AI spend to a single run independently of the
server's cumulative totals. Both features are best-effort and never disrupt the
degradable offload path.

Also fixes the `login` credit display to check `unlimited` before the numeric
balance, so comped/internal accounts don't incorrectly show "0 remaining".
2026-06-18 21:10:10 -05:00
Colby McHenry c9e207a0f2 feat(cli): add codegraph usage command to show AI balance and recent usage
Adds a `usage` subcommand that pings `/v1/usage` with the stored token and
displays balance, plan, 30-day explore/token counts, and allowance reset date.

Degrades quietly in all non-happy-path states — signed out, BYO endpoint, or
unreachable server — so managed reasoning remaining optional doesn't change.

Also extends `OffloadUsage` with the fields the endpoint already returns
(`unlimited`, `banned`, `tokensLast30`, `callsLast30`, `creditsLast30`) that
were previously untyped.
2026-06-18 01:22:58 -05:00
Colby McHenry 193722de45 feat(cli): replace offload subcommands with browser device-authorization login / logout
The old `offload` command family required users to paste a token manually (`offload login --token `) and exposed bring-your-own-endpoint plumbing (`set-endpoint`, `status`, `disable`) as top-level CLI surface. This replaces it with a standard OAuth device flow (RFC 8628 shape) against the CodeGraph dashboard.

`codegraph login` calls `/api/cli/device/start`, opens the browser to the returned URL, polls `/api/cli/device/token` until the user approves, then stores the minted token and enables managed reasoning. `codegraph logout` clears it. BYO-endpoint configuration moves entirely to env vars (`CODEGRAPH_OFFLOAD_URL` / `CODEGRAPH_OFFLOAD_KEY` / `CODEGRAPH_OFFLOAD_MODEL`), keeping the CLI surface minimal.
2026-06-18 00:15:40 -05:00
Colby McHenry 8aa05380a2 Merge branch 'feat/offload-byo' into codegraph-ai 2026-06-17 23:48:10 -05:00
Colby McHenry 3ba82681ea Merge branch 'fix/explore-corroboration-ranking' into codegraph-ai 2026-06-17 23:47:50 -05:00
Colby McHenryandClaude Opus 4.8 da5c6c2f79 feat(offload): managed tier (CodeGraph AI) — metered reasoning via org token [WIP]
Adds the managed offload mode: point codegraph_explore at the CodeGraph AI metered
gateway (https://ai.getcodegraph.com) with an org token instead of a BYO provider key.
Same synthesis client, pointed at codegraph-ai-proxy (a metered OpenAI-compatible gateway).

- credentials.ts — org token in ~/.codegraph/credentials.json (0600); unlike a BYO
  provider key it's a revocable org-scoped auth token (gh/npm-login style), kept out
  of config.json
- config.ts — managed branch in resolveOffload: default gateway URL + public model id
  (openai/gpt-oss-120b) + login token as bearer; managed requires a token to be enabled
- reasoner.ts — fetchUsage() reads the credit balance from /v1/usage
- bin/codegraph.ts — `codegraph offload login --token <t>` / `logout`; status shows the
  managed tier + live balance

Proven GREEN end-to-end against a local wrangler-dev of the proxy: org token validated,
credits prechecked, real Cerebras synthesis returned, and credits metered + charged
(250,000 → 248,473). Graceful degrade on upstream failure; balance via /v1/usage.
Phase 3 (codegraph login device flow) replaces the manual --token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:06:42 -05:00
Colby McHenryandClaude Opus 4.8 db4c9f3641 feat(offload): reasoning offload for codegraph_explore (bring-your-own endpoint)
codegraph_explore can now hand the source it retrieved to a reasoning model you
point at — any OpenAI-compatible endpoint (Cerebras, OpenAI, a local vLLM/Ollama)
with your own key — and return that model's tight, cited answer instead of the
raw source dump. The agent's main context gets the answer in far fewer tokens, at
the cost of one network round-trip.

Off by default. Configure with `codegraph offload set-endpoint <url> --model <m>
--key-env <ENV>` (or the CODEGRAPH_OFFLOAD_* env vars); status/disable manage it.
The API key is never written to disk — the config stores the NAME of an env var
and the key is read from it at call time. Strictly degradable: any failure
(no endpoint, network, timeout, empty answer) returns null and the call falls
back to the local source, so the offload can never surface an error to the agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:18:22 -05:00
Colby McHenryandClaude Opus 4.8 798cd0e21c fix(explore): keep multi-term backend files from being buried by a denser frontend layer
codegraph_explore's file sort is primarily driven by Random-Walk-with-Restart
graph-centrality mass, seeded from the query's text matches. In a cross-layer
monorepo (an API server alongside a much larger, internally dense frontend that
mirrors the same domain words), that mass skews to the bigger layer — so a
backend service/handler that genuinely matches several query terms, even when
it's the #1 search hit, sorts below hits=0 frontend files and gets truncated out
of the response, and the agent reads it back.

Add a corroboration tier above the graph signal: a file that is BOTH an
entry/central file AND matched by >=2 distinct query terms is kept in. The
entry/central guard prevents an incidental multi-term file (a type/util file
that isn't the flow) from displacing a graph-central answer file — a blunt
hits-only tier regressed that case. Single-layer repos are unaffected. Gated by
CODEGRAPH_RANK_NO_MULTITERM=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:44:39 -05:00
Colby MchenryandGitHub f34f606342 feat(extraction): same-file value-reference edges for impact analysis — 15 languages (#897)
Adds same-file value-reference edges (reader symbol → const/var it reads) so impact analysis catches a constant's same-file consumers, closing the 'change this table, break its readers' hole. 15 languages validated S/M/L on public OSS: TS/JS/tsx, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, Pascal/Delphi (+ Svelte/Vue/Astro inherited). Edges-only — node count identical on/off; default ON, CODEGRAPH_VALUE_REFS=0 opts out.
2026-06-16 12:16:00 -05:00
2f6316500d feat(extraction): enable same-file value-reference edges by default (TS/JS) (#895)
Value-reference edges (same-file `references` edges from a reader to the
file-scope const/var it reads) shipped behind CODEGRAPH_VALUE_REFS pending an
agent A/B. The A/B is in: on excalidraw the edges are correct and precise (node
count unchanged) and they transform the impact/blast-radius API — `impact` on a
const consumed by 103 readers goes from 1 affected symbol to the full radius.
That blast-radius API is what `codegraph impact` and CodeGraph Pro's verdict
engine consume, so the win is impact correctness; the agent path showed no
regression. Flip the default on; CODEGRAPH_VALUE_REFS=0 disables.

Also close the one precision gap the A/B surfaced: a bundled/Emscripten
`const Module` re-declared as an inner `var Module` / param produced false
positives (nested readers resolve to the inner binding). isGeneratedFile() is
path-only and can't catch content-minified bundles, so prune SHADOWED targets at
the syntax level — drop any value-ref target whose name is bound by more than one
`variable_declarator` in the file. On excalidraw this removes the 23 false
positives while preserving every real reader (impact unchanged at 170).

Adds regression coverage (there was none): same-file readers are edged, they
surface in the impact radius, shadowed consts are NOT edged, and
CODEGRAPH_VALUE_REFS=0 emits nothing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:28:00 -05:00
b49147eab0 fix(cli): make codegraph index a full rebuild so it stops reporting 0 nodes (#874) (#894)
`codegraph index` ran extraction against the already-populated DB without
clearing it first. On an unchanged tree every file's content hash still
matched, so the orchestrator skipped re-inserting all of them and the run
reported its delta (after - before = 0) as "0 nodes, 0 edges" — which read as
if `index` had wiped the graph. `init` only ever differed because it runs on a
freshly created, empty DB.

Clear the existing graph before re-indexing so `index` rebuilds from scratch
and reports the same complete result as a fresh `init`. `--force` keeps its
role as the home-dir/root-path override; `sync` stays the incremental path.

Adds an end-to-end regression test driving the built binary (init -> index),
asserting the graph stays populated and the summary is never "0 nodes, 0 edges".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:45:03 -05:00
ab107b325a fix(watcher): warn (don't degrade) on Linux inotify watch exhaustion (ENOSPC) (#893)
On the Linux per-directory watch path, hitting fs.inotify.max_user_watches
surfaces as ENOSPC — which the degrade logic added for #876 (EMFILE/ENFILE
only) did not catch, so it fell through to the silent "skip this directory"
branch: a large repo got a partial watch set with no hint why edits in
unwatched directories stopped auto-syncing.

ENOSPC is non-fatal — raise the limit and partial watching keeps working — so
it now warns ONCE, naming the exact knob (fs.inotify.max_user_watches, with the
sysctl to set it), instead of degrading. It also stops attempting further doomed
watches for the session (every inotify_add_watch would fail too). Installed
watches keep firing; `codegraph sync` / git sync hooks cover the remainder.

Validated on macOS (forced per-directory path) and real Linux (Docker) — the
new test asserts a single warning naming fs.inotify.max_user_watches, no
degrade, and a live partial watch.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:01:05 -05:00
beca7116a0 feat(mcp): surface degraded watcher state to the agent in tool responses (#892)
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.

Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.

Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:47:31 -05:00
cea4d086f9 fix(watcher): degrade cleanly on watch exhaustion and prolonged lock contention (#891)
The live file watcher could stay "alive" after it had stopped being
trustworthy. EMFILE/ENFILE watch-resource exhaustion only logged (and was
silently tolerated on the Linux per-directory path), and prolonged
LockUnavailableError retried forever at the normal debounce cadence — both
left auto-sync dead while the index silently drifted stale. Especially bad
for long-running MCP/daemon sessions.

Add a one-way degrade(): on watch-resource exhaustion (any watch strategy)
or on lock contention past a bounded exponential-backoff budget, log once,
fire a new onDegraded callback, and stop. start() now returns false
consistently when the per-directory path degrades at startup — it previously
returned true on Linux, so the MCP server reported the watcher "active" when
it had degraded. Wire onDegraded into the MCP server so callers are actually
told, and expose isDegraded()/getDegradedReason().

Builds on the approach in #877 by @thismilktea. Validated on macOS
(recursive), Linux (per-directory, Docker) and Windows (recursive) — 30/30
watcher + watch-policy tests on each.

Closes #876

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:32:54 -05:00
Colby McHenry 1bd9431879 Merge branch 'feat/value-reference-edges' 2026-06-14 18:39:35 -05:00
Colby McHenryandClaude Opus 4.8 ec90ddf79a feat(extraction): same-file value-reference edges (flag-gated)
Emit 'references' edges from a symbol to the file-scope const/var it reads
(TS/JS), so impact analysis catches "change this table, affect its readers".
Off by default behind CODEGRAPH_VALUE_REFS pending the agent A/B; on a real PR:
+3.1% edges, 100% precision on the spot-checked target, 372/372 extraction tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:25:34 -05:00
b35a292c90 chore(release): bump version to 1.0.1 (#869)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:06:55 -05:00
5cc155ddc4 fix(index): skip nested git worktrees instead of indexing them as duplicate repos (#848) (#868)
A git worktree nested in a project (e.g. Claude Code's gitignored
`.claude/worktrees/<name>/`) was swept into the index as an embedded repo: its
`.git` is a FILE pointing into the host repo's `.git/worktrees/`, and embedded-
repo discovery treated any `.git` (file or directory) as a distinct repo to
index. Each worktree then duplicated the entire graph — one report went from
~1,850 files to 24,533, with search/explore flooded by stale copies.

classifyGitDir() now distinguishes:
- `.git` directory       -> embedded clone, index (#193/#514/#622, unchanged)
- `.git` file → worktrees/ -> worktree, skip (#848)
- `.git` file → modules/   -> submodule, index (unchanged)

Applied at both embedded-repo entry points: findNestedGitRepos discovery (which
also covers the sync/change-detection path) and the untracked-subdir recursion
in collectGitFiles.

Verified: the reproduction drops from 6 files / betaHelper×3 to 3 files / ×1,
with a genuine embedded clone and submodules still indexed. Regression test added.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:01:00 -05:00
64ff7597d0 fix(cli): stop serve --mcp from confusing humans — hide it + explain on a TTY (#867)
`codegraph serve --mcp` is the stdio MCP server an AI agent launches for itself
(the installer wires it into every agent's MCP config), not a command a human
runs. Run by hand in a terminal it just hung waiting for JSON-RPC, looking
broken.

- Hide `serve` from `--help` (commander `{ hidden: true }`); it stays fully
  invocable, so agents are unaffected.
- When stdin is an interactive TTY (a person — never the agent's pipe or the
  detached daemon), print what it is and point to `codegraph status` /
  `codegraph daemon`, then exit instead of hanging.
- README: drop `serve --mcp` from the CLI Reference and stop the troubleshooting
  section from telling users to run it; keep the accurate "your agent launches
  it" note.

Verified: agent path intact (22 MCP handshake/daemon tests pass), `serve` absent
from --help, and the TTY path prints the message and exits cleanly.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 15:18:55 -05:00
f7441f2124 fix(resolution,cli): cross-file static method calls + affected path normalization (#825) (#865)
Cross-file `ClassName.staticMethod()` calls resolved to the class, not the
method: the import resolver matched the receiver `Foo` to the named class
import but dropped the `.bar` member, and createEdges then mis-promoted the
`calls` edge to `instantiates`. So callers/impact for the static method came
back empty. Descend from the resolved class into its `Container::member` so the
call links to the method; fall back to the class when no such member exists
(non-`::` languages and genuine class references are unaffected).

Also normalize `codegraph affected` inputs to the project-relative,
forward-slash form the index stores, so `./src/x.ts`, an absolute path, and a
Windows back-slash path all match (previously silently returned 0).

Validated on luxon (24 files): node/edge totals identical (no explosion), 69
mis-promoted `instantiates` edges become `calls`, and real static factories
(DateTime.fromISO, etc.) resolve their callers. Full suite: 1534 passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:52 -05:00
fb974552b0 docs(readme): point existing users to codegraph upgrade under the 1.0 banner (#866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:48 -05:00
070ce4da2b feat(cli): codegraph version command + complete CLI Reference (#864)
* feat(cli): codegraph version command + complete CLI Reference

Add a `codegraph version` subcommand plus the `-v` and `-version`
spellings (commander already wires up `--version`/`-V`), so the version
is easy to reach however a user guesses at it. The `-v`/`-version` forms
are intercepted before commander parses — its version short flag is the
capital `-V`, and its parser rejects a multi-character single-dash flag.
A trailing `-v` on a subcommand still means `--verbose`.

Document the previously-missing commands in the README CLI Reference:
`daemon`/`daemons`, `unlock`, `telemetry`, `version`, and `help`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): reference #864 on the version-command entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:14:32 -05:00
Colby MchenryandGitHub ff288ac711 feat(cli): one interactive codegraph daemon command, replaces stop/list (#863)
Collapses the unreleased daemon controls into a single interactive command.
`codegraph daemon` (alias `daemons`) opens an arrow-key picker (current project's
daemon first, pre-selected), enter stops it, or pick "Stop all"; non-TTY prints a
plain list. Removes stop/list/ps; reuses the unchanged daemon-registry machinery;
the pick->stop loop is in daemon-manager.ts behind an injectable select (unit
tested). Validated live on macOS/Linux (real clack picker driven via pty) and
Windows (real runDaemonPicker + stopDaemonAt against a real daemon). Closes #845
follow-up.
2026-06-13 13:53:38 -05:00
Colby MchenryandGitHub 0f825649a1 feat(cli): codegraph stop / list to manage background daemons (#861)
Adds first-class daemon control (the #845 pain point: no clean way to stop a
runaway daemon). `codegraph stop [path]` stops the current/given project's
daemon (SIGTERM -> SIGKILL fallback, sweeps artifacts); `stop --all` stops every
daemon; `list`/`ps` shows running daemons (--json for scripts).

Discovery via a small self-healing registry: each daemon records its root under
~/.codegraph/daemons/ on start, removes it on graceful shutdown; readers prune
dead pids. Cross-platform by construction (files + process.kill). Validated live
on macOS, Linux (docker), and Windows (VM): registry unit 6/6 and real-daemon
stop/list 6/6 on each.
2026-06-13 12:59:41 -05:00
Colby MchenryandGitHub 2472508549 fix(installer,cli): refuse to index $HOME / filesystem root (#860)
Running the installer or `codegraph init`/`index` from $HOME auto-indexed the
entire home tree (installer indexes process.cwd() with no guard), producing a
multi-GB ~/.codegraph/codegraph.db; the install dir sharing the ~/.codegraph
name then made every home subdir resolve its root to $HOME. On pre-1.0 macOS the
per-file watcher over that tree exhausted kern.maxfiles and crashed the machine
(#845; the fd blowup was fixed in 1.0.0, this fixes the root cause).

Add unsafeIndexRootReason() and refuse the home dir, a parent of home, and
filesystem roots at the installer auto-index, `init`, and `index`. Overridable
with --force. Closes #845.
2026-06-13 12:35:18 -05:00
Colby MchenryandGitHub 484da77296 test(mcp): make liveness-watchdog kill assertions cross-platform (#859)
Validated the watchdog on the Windows VM: it kills a wedged process correctly,
but Windows has no real signals — process.kill(pid,'SIGKILL') maps to
TerminateProcess, seen as signal=null + non-zero code, not 'SIGKILL'. Assert
"killed" platform-agnostically and require the own exit code in the opt-out test.
Source watchdog unchanged. Windows: fatal-handler 8/8, liveness-watchdog 7/7,
mcp-daemon 9/9; mcp-initialize EPERM is pre-existing (identical with watchdog off).
2026-06-13 11:51:57 -05:00
Colby MchenryandGitHub 1702dfc544 fix(mcp): make the liveness watchdog a separate process, not a worker thread (#858)
The worker-thread watchdog from #856 didn't work in the real daemon — caught by
live-testing against a real serve --mcp. V8 isolates coordinate on global
safepoints, so a main thread wedged in a tight non-allocating loop (#850's
SourcePositionTableIterator::Advance) strands the watchdog worker before it can
SIGKILL.

A separate child process shares no isolate/heap with the parent, so the wedge
can't touch it; it kills via the kernel. Parent heartbeats to the child's stdin;
silence past the timeout -> SIGKILL; parent exit closes the pipe -> child exits.
Validated live (real daemon SIGKILLed in ~timeout); regression test covers the
non-allocating-wedge-under-heap-pressure case. API/install points/CHANGELOG
unchanged; the broken worker version was never released.
2026-06-13 10:40:55 -05:00
Colby MchenryandGitHub 576149e062 feat(mcp): worker-thread liveness watchdog to self-kill a wedged main thread (#856)
Belt-and-suspenders follow-up to #855. Any non-yielding sync loop on the main
thread wedges the event loop, and nothing running on that loop (timers, signal
handlers, PPID watchdog) can recover it — only another thread can.

A tiny worker thread (in the detached daemon + direct modes) watches a
shared-memory heartbeat the main thread bumps each event-loop turn; if it stops
advancing across enough consecutive checks (~CODEGRAPH_WATCHDOG_TIMEOUT_MS,
default 60s) the worker SIGKILLs the process so a fresh daemon starts on the next
connection. Counts consecutive stale checks (not wall-clock) so it's immune to
clock jumps / sleep; tuned never to fire on real work; opt out with
CODEGRAPH_NO_WATCHDOG=1.
2026-06-13 10:04:40 -05:00
Colby MchenryandGitHub 3476ac9a27 fix(mcp): exit on uncaught exception instead of orphaning/spinning at 100% CPU (#855)
The process-wide uncaughtException handler logged the error and kept running. For
the detached `serve --mcp` daemon that turned any escaped fault into an
unrecoverable orphan: nothing respawns it, and when logging the raw Error hit a
V8 source-position loop while lazily formatting `.stack`, the main thread wedged
at 100% CPU so even the PPID watchdog / idle-timer could no longer fire. Same
failure mode as #799, which only fixed the stdin-'error' trigger.

Restore Node's default fatal semantics: render a bounded, hang-proof line (name +
message only — never read `.stack`) then exit non-zero, so a fresh daemon starts
on the next connection. Extracted to src/bin/fatal-handler.ts with injectable
seams; unit-tested incl. the never-touch-stack invariant.

Closes #850.
2026-06-13 09:48:44 -05:00
06e03758af docs(readme): collapse the npm-install alternative into a details section (#844)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:01:12 -05:00
13027b0730 docs(readme): auto-sync becomes quick-start step 4 heading (#843)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:00:23 -05:00
eed0b5ae20 docs(readme): init indexes by default (drop -i) + bold auto-sync guarantee (#842)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:57:50 -05:00
b9eff08c77 chore(release): 1.0.0 — README banner + X account (#840)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:21:46 -05:00
06a410e9b4 feat(extraction): R language support (#828) (#839)
R has no declaration syntax — everything is an expression — so the
extractor works through the visitNode hook: functions in every
assignment form (incl. nested, attributed to their enclosing scope),
top-level variables/constants, library()/require() imports and
source() file references (claimed, Lua-style), S4/RefClass/R6/ggproto
classes with their methods and extends edges, setGeneric/setMethod.
Grammar vendored from r-lib/tree-sitter-r v1.2.0 (ABI 14; npm package
is a security placeholder, tree-sitter-wasms has no R).

Benchmarked on AnomalyDetection (8/8 named defs), dplyr (1027 fns),
ggplot2 (150 ggproto classes / 597 methods / 128 extends edges —
adding ggproto mid-bench flipped the large-repo A/B from a regression
to 2.4x faster than the no-codegraph arm).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:17:29 -05:00
2c7bbd5387 fix(extraction): C# record-struct kind fidelity + bodiless positional records (#831 follow-up) (#838)
The shipped grammar parses every record form as record_declaration (no
record_struct_declaration node), so 'record struct' mis-kinded as class.
classifyClassNode now distinguishes the value-type form by its struct
keyword child, and extractStruct accepts bodiless positional records
(the no-body gate is for C/C++ forward declarations) instead of
crashing mid-file on them.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:17:25 -05:00