GitNexus/gitnexus
Gergő Magyar b06ba169cf fix(mcp): stop scaling the detect_changes query with the diff's hunk count (#2915) (#2930)
* fix(mcp): map diff hunks to symbols without per-hunk OR conditions (#2915)

`detect_changes` folded one `(n.startLine <= $hunkEndI AND n.endLine >=
$hunkStartI)` pair per diff hunk into a single WHERE clause, one query per
changed file. A machine-generated file (cache JSON, lockfile, golden fixture)
diffs at thousands of hunks with `-U0`, and the expression tree that produces
overflows LadybugDB's recursive evaluator copy on a TaskScheduler worker
thread: a bare SIGBUS with no error output where secondary threads get 512 KB
of stack (macOS), a swallowed 30s query timeout where they get more (Linux),
which the CLI then printed as "No changes detected." with exit 0.

Coalesce each file's hunks into sorted, disjoint ranges and run the overlap
test in JS instead. Only ranges that overlap or abut are merged, so the union
covers exactly the lines the raw hunks covered. Query text and parameters are
now identical whether a file changed in 1 place or 100,000, and files are
queried in batches of 100 rather than one full node scan each.

Reproduced on Linux by running the engine with macOS-sized (512 KB) thread
stacks: 2,500 hunks passed, 3,333 and 4,000 segfaulted — matching the reporter's
macOS threshold table. After the change the same repo maps a 100,001-hunk diff
in 2.1s with no crash.

Also fixes a line-base mismatch the rewrite exposed: graph rows are 0-based
(#2377) while git hunk lines are 1-based, so the raw comparison shifted every
symbol one line up. An edit to a symbol's LAST line reported nothing changed —
a one-line function whose body was edited was invisible to the pre-commit gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* fix(cli): say when a detect_changes result is partial (#2915)

When a graph query fails, `detect_changes` swallows the error, sets
`partial: true` and leaves the counts at zero (#2283). The CLI formatter never
read that flag, so a degraded run printed "No changes detected." and exited 0 —
the pre-commit safety gate reporting a clean bill of health for a check that
did not complete. Print the partial note in both the empty and non-empty
branches.

Also restore the `Symbol` placeholder for rows whose label came back as an
empty string: the changed-symbol mapping now keeps `''` instead of dropping it
to undefined, so the formatter needs `||`, not `??`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor(mcp): bound the hunk→symbol query and simplify the overlap helpers (#2915)

Cleanup pass over the #2915 fix. No change to which symbols detect_changes
reports, except that a node matched by two changed paths is now reported once.

* Push a per-file [lo, hi] span into the query. Coalesced ranges are sorted and
  disjoint, so a file's whole touched span is free, and the engine can drop the
  symbols outside it instead of shipping every row in the file across the native
  boundary. Measured on a 400-file batch against a 25k-node index: 546ms/13,870
  rows before, 84ms/1,555 rows after, identical kept set. Depth stays constant
  (two comparisons per file, not per hunk), so #2915 cannot come back — the JS
  test still rejects symbols landing in the gaps between hunks. The struct-list
  parameter was verified against @ladybugdb/core 0.18.3 and 0.19.1.
* Convert hunks into the graph's 0-based space once, at the point they are
  grouped, with the existing `toZeroBasedLine`. Every comparison downstream is
  then base-neutral, and `toDisplayLine` goes back to being what its doc says it
  is: an MCP response-boundary converter, not a filter input.
* Deduplicate matched nodes by id. `ENDS WITH` is a plain string suffix, so a
  diff touching both `README.md` and `pkg/README.md` counted the same node
  twice (169 duplicates in 13,870 rows on a real 400-file diff). Pre-existing,
  free to fix now that the rows are shaped in one place.
* Drop the positional `?? sym[N]` row fallbacks in this block.
  `executeParameterized` returns `getAll()` rows, which are alias-keyed objects,
  so the fallbacks were dead — and they coupled the mapping to RETURN column
  order, which is what made adding a column a renumbering exercise.
* Build the path→hunks map in one pass, so "every value is coalesced" holds at
  every point rather than being repaired by a second loop. Simplify
  `coalesceHunks` (the length<2 branch and the sort tiebreaker changed nothing)
  and state `hunksOverlapRange` as a standard half-open lower bound.
* Document `partial` in the detect_changes tool description. The CLI now prints
  it, but the MCP client — the main consumer of the pre-commit gate — was
  getting the flag as an undocumented raw key.
* Tests: pin the query text as identical for a 1-hunk and a 3,000-hunk diff
  (replacing a magic length bound), pin the 0-based bounds parameter, pin the
  dedup, and fold two near-identical row mocks into one helper. Temp dirs now
  come from the shared pool helper, whose cleanup is per-directory and
  Windows-lock aware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* feat(mcp): bound and batch the hunk→symbol query, and anchor its path match (#2915)

Follow-up review pass on the #2915 fix, implementing every remaining finding.

* Push a per-file `[lo, hi]` span into the query. Coalesced hunks are sorted and
  disjoint, so a file's touched span is free, and the engine drops the symbols
  outside it instead of shipping every row in the file across the native
  boundary. Measured on a 25k-node index, 400-file batch: 546ms/13,870 rows
  before, 84ms/1,555 after, identical kept set. Depth stays constant (two
  comparisons per file, not per hunk), so #2915 cannot return. The struct-list
  parameter was probed against @ladybugdb/core 0.18.3 and 0.19.1 first; the
  index-subscript form `$paths[i]` does not parse.
* Anchor the path match: `n.filePath = b.path OR n.filePath ENDS WITH b.suffix`
  where suffix is the path with a leading separator. A bare `ENDS WITH` is a
  plain string suffix, so a diff touching `lib/a.py` also reported a symbol from
  an indexed `src/mylib/a.py` — a file the diff never touched. This is the form
  `explain` already uses. Pinned by an integration test against a real engine
  (it fails 3/3 with the un-anchored predicate).
* Run batches a few at a time. `executeParameterized` checks a connection out of
  the 8-connection per-repo pool for the duration of a query, so parallel calls
  never share one — the same reason ~15 other queries in this file already run
  under `Promise.all`. `allSettled`, so one failed batch degrades the result to
  `partial` instead of discarding the batches that succeeded beside it.
* Deduplicate matched nodes by id, and count `changed_files` as distinct paths:
  a path can appear twice in one diff (a rename reported alongside an edit).
* Cap the listed symbols at 1,000 with `symbols_truncated: {listed, total}`.
  A repo-wide diff otherwise puts an unbounded array in one MCP payload — the
  CLI has `--limit`, an MCP client has nothing. Counts are never capped, so the
  risk level and the CLI's "... and N more" still see the true total.
* Extract `chunk` / `mapBatches` / `LBUG_QUERY_BATCH_SIZE` into
  `core/lbug/query-batch.ts`. Every query built from a caller-sized array has
  this ceiling; the shape now has one name and the measured batch size is
  recorded where it is defined rather than in three constants under three names.
* Move hunk grouping and the 0-based conversion into `coalesceHunksByPath`, at
  the parse boundary. `parseDiffHunks` stays faithful to git (1-based, like the
  `@@` headers it reads), consumers compare graph-native values, and the
  conversion is unit-testable instead of living in the backend.
* Document `partial` and `symbols_truncated` in the detect_changes tool
  description — the MCP client is the main consumer of the pre-commit gate and
  was getting both as undocumented raw keys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor(core): batch every remaining repo-sized query list (#2915)

`detect_changes` was not the only place building query text from a
caller-sized array. `core/wiki/graph-queries.ts` interpolated the whole file
list of a module into four `IN [...]` literals, growing the query with the
repo — flat breadth rather than the nested depth that crashed #2915, but the
same unbounded shape, and the one the repo's own `DELETE_FILES_CHUNK_SIZE`
precedent already chunks elsewhere.

All four now run one query per batch and merge in JS. The membership arms need
care, and each is documented where it happens:

* `getIntraModuleCallEdges` batches the caller arm only. A per-batch callee arm
  would drop a call from batch 0 to batch 2, both inside the module, so that
  predicate moves to JS against the whole set. Results are now sorted: the
  single-query form had no ORDER BY, and batch order would hand the entire
  30-edge window `formatCallEdges` keeps to the first 100 files (#2787).
* `getInterModuleCallEdges` keeps the SAME batch list in its `NOT` arm. That is
  sound — a file outside the module is outside every batch — and it preserves
  the null handling: `NOT null IN [...]` is null, so the original dropped edges
  to a node with no filePath, where a JS-only `!has(undefined)` would admit
  them. ORDER BY and LIMIT move to JS because a per-batch limit would cut rows
  before the cross-batch membership filter ran.
* `getProcessesForFiles` keeps `LIMIT` inside the batch: `stepCount DESC, id` is
  a total order, so a process in the global top-N is in its own batch's top-N.

Also adopt the shared `chunk()` at the hand-rolled slice loops in
`lbug-adapter.ts`, `embeddings/http-client.ts` and `run-analyze.ts`. The loops
whose index fed a progress callback or an error message use
`chunk(...).entries()`, which removes the `i / SIZE` and `Math.floor(i / SIZE)`
arithmetic rather than reproducing it. No batch size changed.

One trap that survived tsc and is worth naming: after renaming a loop variable
away from `chunk`, a leftover `chunk.length` silently resolved to the imported
FUNCTION's arity, reporting `chunkSize: 1` for a 200-path batch. Only
`lbug-query-importers-batch`'s exact-value assertion caught it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor: name the line-base conversions and share the symbol line (#2915)

The 0-based-graph vs 1-based-elsewhere rule was open-coded in five places with
the reasoning living only in comments — the same rule that, applied by hand and
skipped once, hid every last-line edit from `detect_changes`.

* Add `toOneBasedLine` beside `toZeroBasedLine` in `ingestion/utils/line-base.ts`
  so the module owns both directions, and adopt it at the four CFG/PDG join
  sites in `pdg-impact.ts` and the two in `local-backend.ts`. This is NOT
  `line-display.ts`'s `toDisplayLine`, which is documented as a response
  boundary converter with an `undefined` passthrough; the joins need
  arithmetic, and the guards that produce `Number.NaN` for an absent line are
  kept verbatim.
* `http-route-extractor.ts` probed graph spans with a bare `line - 1` and a
  20-line comment. It calls `toZeroBasedLine` now; the `?? pick(line)` fallback
  arm is untouched, so which node is picked cannot change (the clamp differs
  only for a negative line, which no emitter can produce).
* Extract `formatSymbolLine`: `detect-changes-format.ts` and `eval-server.ts`
  rendered the same `type name → filePath` line. One behavior note — the two
  were not byte-identical, and eval-server had no placeholder on `name`, so a
  definition with an empty name rendered the literal `undefined` and now
  renders `?`. Both `definitions[]` shapes set name from a graph row, so this
  is unreachable in practice, and printing `undefined` into LLM-facing output
  is the bug, not the intent.

`||` (not `??`) in the placeholders is deliberate and documented: a node label
can come back as an empty string and still needs the placeholder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* perf(wiki): bind the module file list instead of splicing it into the query (#2915)

The wiki's four `IN [...]` sites interpolated every file of a module into the
query text, so the text grew with the repo — the shape that overflowed
LadybugDB's recursive evaluator copy in `detect_changes`. The previous commit
chunked them, which worked but cost real complexity: the callee arm had to leave
Cypher and be re-implemented in JS, DISTINCT had to be re-established across
batches, and ORDER BY/LIMIT had to move to JS so a per-batch window could not cut
rows the cross-batch filter still needed.

Binding the list as a parameter removes the reason for all of it. The text is
constant at any list length, and measured against a real index a bound list is
~3x faster than the equivalent literal (5,000 items: 139ms vs 459ms; 20,000:
598ms vs 1,686ms). Every predicate goes back into Cypher, including the `NOT ...
IN` arms whose null handling is load-bearing — `NOT null IN [...]` is null, so a
callee with no filePath is dropped by the engine, where a JS membership test
would have admitted it.

Verified on this repo's own index: a 2,000-path bound list returns 14,856 rows in
877ms.

Also collapses the per-process step query into one grouped `p.id IN $ids` fetch —
105ms to 13ms for 20 processes — and drops `fileListLiteral`, `callEdgeKey`,
`compareProcessHeaders` and the batching loops with it. `compareStrings` was a
byte-identical re-roll of `compareCodeUnits` (src/lib/utils.ts), including its
#2787 rationale; it now calls the shared one.

Intra-module edges are sorted where the original had no ORDER BY: `formatCallEdges`
keeps only the first 30, and an unordered cut keeps a different subset per machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor(core): one home for batching, and a backstop for the shape that crashed (#2915)

`chunk` moves to `src/lib/utils.ts`, the repo's generic-utility home: it is an
array helper, and leaving it in `core/lbug/query-batch.ts` made an HTTP
embedding client import batching from the graph-DB namespace. `query-batch.ts`
keeps what is actually about queries — the measured `LBUG_QUERY_BATCH_SIZE`, the
concurrency helper, and the ceiling — and now documents the preference the wiki
change proved: bind the list as a parameter first, chunk only when you cannot.

`mapBatches` becomes `mapConcurrent`: nothing about it is batch-specific, and it
now has non-query callers. Its body is a per-item try/catch plus `Promise.all`,
so ordering comes from the primitive rather than from unwrapping a settled
union. The wave barrier stays — measured against a rolling window it is 538ms vs
532ms on a 1,000-file diff, whose per-batch times spread only 1.35x.

Adopted at the loops that were still hand-rolled: `file-hash.ts`,
`cluster-enricher.ts` (its progress callback now accumulates `batch.length`
instead of clamping an index), `filesystem-walker.ts` and `language-config.ts`
(wave scheduling with `allSettled`, which is exactly `mapConcurrent`).
Deliberately not adopted, each for a stated reason: the analyzer-identity probe
runs as a standalone `node -e` script with no module resolution; the embedding
sub-batch loop slices two parallel arrays and breaks early;
`walkRepositoryPaths` reports progress from inside each wave, which
`mapConcurrent` cannot express.

`warnIfQueryTextUnbounded` is the backstop: #2915 died in native code with no
message, and a query built by concatenating a caller-sized list is the shape
that gets there. Wired at both execution chokepoints (`pool-adapter`'s
`executeParameterized`, `lbug-adapter`'s `executePrepared`/`streamQuery`; their
`executeQuery` siblings delegate and are covered once). It never throws — a long
query the engine can actually run must not start failing on a heuristic — and it
is deliberately absent from the raw write path, where a node's `content` is
inlined and a large source file would warn legitimately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor(mcp): name the path-match rule, and key detect_changes by node id (#2915)

* `path-predicate.ts` names the three ways a caller's path can match a stored
  `filePath` — `exact`, `pathSuffix`, `fragment` — instead of each call site
  copying whichever idiom its neighbour used. A bare `ENDS WITH` is a plain
  string suffix, which is how a diff touching `lib/a.ts` came to report a symbol
  from `src/mylib/a.ts`; the loose `CONTAINS` sites are loose ON PURPOSE (a user
  hint of `src/mcp` should match a directory fragment), and naming the modes is
  what lets a call site choose rather than inherit.
* `detectChanges` kept four structures over one row set — an array, a dedup Set,
  an id list and an id→name Map — that had to stay in sync by hand. One
  id-keyed Map is all of them; insertion order is preserved, so every output is
  byte-identical.
* `symbols_truncated: {listed, total}` becomes `truncated: true`, the key
  `explain`/`pdg_query`/`trace` already use. The true total was always in
  `summary.changed_count`, so the nested object said nothing the existing
  vocabulary could not.
* `GraphLineRange` is now a distinct type from `DiffHunk`: they carry the same
  two fields in different bases, and mixing them IS #2377. The name means a
  1-based hunk cannot reach `hunksOverlapRange` without a conversion between.
* `coalesceHunksByPath` accumulates raw ranges and coalesces once per path
  rather than re-sorting on every occurrence.
* `chunk` adopted at this file's own five loops — the point of extracting it —
  including two locals named `chunk` that shadowed the import. That shadowing is
  not cosmetic: it is how a leftover `chunk.length` silently became the
  function's arity earlier in this branch.

One bug caught by the real-engine integration test and worth naming: Cypher
comments are `//`, not `--`. A `--` comment inside the query string made
LadybugDB reject the whole query at PREPARE, which `detect_changes` swallows
into `partial` and renders as "No changes detected." Every mocked unit test
passed. Prose stays out of query strings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* refactor(test): share the git-repo bootstrap, and move the shared formatter out (#2915)

`formatSymbolLine` lived in `detect-changes-format.ts` but is rendered by
`eval-server`'s query formatter too, so a `query` formatter imported from a
`detect_changes` module. It moves to `src/cli/format-symbol.ts`; both callers
import it from there. The `||`-not-`??` fallbacks stay documented — a node label
can come back as an empty string and still needs its placeholder.

`test/helpers/temp-git-repo.ts` gives `initGitRepo(dir, identity?)` and
`commitAll(dir, message)` to the ~10 test files that hand-rolled the same
`git init -q` + two `git config` + `add -A` + `commit` sequence. It takes a
directory and never owns one, matching `temp-dir-pool.ts`'s split of lifecycle
from seeding; the identity is a parameter because the existing consumers
genuinely disagree about it, and each keeps exactly what it configured. Four
files stay hand-rolled for stated reasons — pinned author dates for a
deterministic digest, remote handling, `--allow-empty`, and the `-c key=value`
form that never persists to the repo.

Test trims: the `formatSymbolLine` fallback cases collapse into one `it.each`
table (the case pinning that BOTH consumers emit the helper's exact line stays —
no table row can express it); two `line-base` cases that were compositions of
their neighbours go; and `detect-changes-path-anchoring` runs its
`detect_changes` call once in `beforeAll` instead of three times, keeping the
three named failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS

* perf(mcp): filter the batched hunk query before the engine materialises (#2915)

`UNWIND $bounds AS b MATCH (n) WHERE …b…` compiles to a CROSS_PRODUCT whose
build side is a RESULT_COLLECTOR over the whole filtered node table: only the
`n`-only predicates get pushed below the accumulate, so neither the anchored
path match nor the [lo, hi] span could reduce the scan. Measured at 1M nodes:
+242 MB for one batch and +922 MB for the four concurrent ones, paid even for a
one-file diff — and at a 268 MB buffer pool the query died with `Buffer manager
exception` where the old per-file query completed, landing in `partial:true` +
`changed_count:0`, the #2915 false clean by another route.

Adding the batch-wide, `b`-free disjunction as a redundant leading conjunct
lets the planner push it below the accumulate: EXPLAIN now shows it as FILTER[2]
directly under SCAN_NODE_TABLE[0]. It is a provable superset of the correlated
predicate, so it cannot drop a row the correlated filter keeps. 10x less memory,
~20% faster, identical result sets.

Also in detect_changes:
- Sort rows on (filePath, startLine, id) before the 1000-symbol cut. The cut was
  slicing engine row order — measured 5 distinct orders across 8 runs on one
  connection, the #2787 class this branch fixes 200 lines away in the wiki.
- Chunk `symIds`, the one caller-sized list left unbatched: 500k ids measured
  4.0 GB RSS. Binding keeps the query TEXT constant, which is all the unbounded
  guard measures, while the bound VALUE stayed repo-sized.
- Prefer exact path equality and widen to the anchored suffix only for paths
  that matched nothing, so a root README.md stops reporting pkg/*/README.md.
- Report `risk_level:'unknown'` rather than 'low' when a query was swallowed.
  A degraded pre-commit gate must not read as an all-clear.
- Pass --no-ext-diff --src-prefix=a/ --dst-prefix=b/. `diff.noprefix` in a user's
  gitconfig makes git emit `+++ f.py`, which parseDiffHunks cannot match, so every
  run printed "No changes detected." and exited 0 before any query ran. A diff
  that parses to zero files now raises `partial` instead of the clean branch.
- `labels(n)`, not `labels(n)[0]`: labels() returns a scalar string here, so the
  subscript was always '' and `type` never carried a label.
- Validate IMPACT_MAX_CHUNKS. The chunk() adoption turned an entry condition into
  an exit condition, so a non-numeric value ran every chunk instead of none.
- Record why four-way concurrency is safe here, and scope the arm64 sequential
  comment to the query it was written for (#496).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* fix(cli): fail the detect_changes gate instead of exiting 0 when it degrades (#2915)

The secondary half of #2915 was that a swallowed query failure printed
"No changes detected." and exited 0, so a shell pre-commit gate passed on a
broken analysis. This branch added the PARTIAL text. It did not change the exit
status, so `gitnexus detect-changes && git commit` still proceeded.

`detectChangesCommand` passed a STRING to `output()`, and `output()` sets a
failing code only for an OBJECT carrying `error` — under a comment calling
itself "the one place that keeps scripted callers honest". A string never
matches, so this command opted itself out of the only mechanism the file
provides. It was broader than `partial`: the formatter also renders a backend
`{error}` payload as text, so hard failures exited 0 too.

Fixed narrowly in `detectChangesCommand`, following the object-first shape
`checkCommand` already uses, rather than widening `output()`'s shared contract —
every one of its other seven callers already passes an object and is unaffected.
One code for both `error` and `partial`: `&&` only distinguishes zero from
non-zero, and a softer code for `partial` would invite `|| [ $? -eq 2 ]`
exemptions that reopen exactly this hole.

`truncated` deliberately stays exit 0 — only the listing is capped, while the
counts and risk are computed over the full set, so the verdict is sound and
failing on it would fire on every large-but-healthy diff.

Also wires `truncated` through the formatter, which this branch had left as a
producer-only flag while `partial` went end to end, with the note in both
locales and no count of its own so the existing "... and N more" line stays the
sole numeric report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* fix(wiki): restore step order and symbol labels, and cut the edge list in Cypher (#2915)

Found by running the queries against a real engine, which nothing did before:
this branch's regrouped `withSteps` returned step traces OUT OF ORDER.
`ORDER BY pid, r.step` combined with `WHERE p.id IN $ids` silently drops the
second sort key — `proc_1_incrementalupdate` came back 2,7,1,3,4,5,6.
`ORDER BY step` alone is correct, and so was the pre-branch per-process query,
so this was introduced by the batching. `formatProcesses` prints
"${s.step}. ${s.name}", so every module and overview page was getting scrambled
execution traces. The mocked suite passed 112/112 before and after.

`labels(x)[0]` is always the empty string: labels() returns a scalar string and
the subscript is 1-based over its characters ([1] is "F"). `prompts.ts` renders
"${s.name} (${s.type})", so all 5,027 exported symbols reached the LLM as
"name ()".

`getIntraModuleCallEdges` shipped every edge to use 30 — measured 18,299 rows
and 851 ms with all 2,079 paths bound, against 30 rows and 94 ms with
ORDER BY + LIMIT in Cypher, which the sibling `getInterModuleCallEdges` twenty
lines below already did. The determinism fix (#2787) was right; the placement
was not. `compareCallEdges` goes with it — it was intransitive when a name was
null or empty, so `Array.sort` was input-permutation dependent, i.e. the
nondeterminism it was added to remove.

Deletes the positional row ABI this branch newly documented. The vendor
declaration is `getAll(): Promise<Record<string, LbugValue>[]>` — string keys
only — and `row[0]` probes back `undefined`; the same PR deleted ~30 identical
fallbacks from local-backend.ts. They were already stale here: `withSteps`
prepends `p.id AS pid`, so `toProcessStep` was reading the pre-branch layout.
Rows are now typed by alias, so renaming an `AS` is a compile error. `??` for
`||` so a step of 0 or an empty label keeps its own value.

Tests: a real-engine integration suite covering all seven exported queries
(PREPARE included — the trap that shipped a `--` comment on this branch), and
the four holes that let the ordering bug through — a vacuous order assertion, a
LIMIT never reached by a 2-edge fixture, a fake that returned rows pre-ordered
and ignored ORDER BY, and a hardcoded `type: 'Function'` that hid labels().

The step-ordering fixture is empirically sized: 2 processes never reproduced the
bug, ~400 step edges was intermittent, 710 (20 processes x 26-45 steps) hit
11 of 11 runs. Seeded descending and interleaved so no grouping looks sorted by
accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* refactor: put the shared helpers where their callers are, and make their contracts true (#2915)

`mapConcurrent` moves to lib/utils.ts beside chunk(). Nothing about it is
query-specific and it already had filesystem callers, while its docstring
justified concurrency safety through the per-repo connection pool — an argument
that does not apply to fs.readFile. This is the precondition the branch's own
commit message stated ("it now has non-query callers") and then did not apply.
LBUG_QUERY_BATCH_SIZE and warnIfQueryTextUnbounded genuinely are query-specific
and stay.

`pathMatch`/`PathMatchMode` deleted: zero callers, and none of the three sites
its docstring cited were migrated, so the tree carried the abstraction and the
copies it was written to replace. `pathSuffixOf` stays and the module now
documents the anchoring rule it actually implements.

Contracts that were not true:
- QUERY_TEXT_CEILING_BYTES was compared against `cypher.length` — UTF-16 code
  units, not bytes — so non-ASCII query text was undercounted and the reported
  KB was wrong. Buffer.byteLength now, behind a `length * 3 <= ceiling` early
  return so only text over ~21 KB pays for the count.
- chunk(items, NaN) returned [[]], against a docstring promising never to return
  an empty slice, and mapConcurrent's Math.max(1, NaN) propagated it — which
  would have resolved [] for non-empty input with no error, read as "no results"
  by every call site.
- GraphLineRange claimed a 1-based hunk could not reach hunksOverlapRange
  without a conversion, but it was structurally identical to DiffHunk so tsc
  accepted one with no diagnostic, and coalesceHunks<T extends GraphLineRange>
  actively laundered the base while its accumulator was still DiffHunk[]. The
  useless generic is gone and a one-line phantom on each interface makes the
  claim real; a bare {startLine, endLine} literal still satisfies both, so no
  construction site needs a cast.

Pure deletions no longer vanish. A -U0 deletion emits `+N,0`, which
parseDiffHunks dropped, so the file survived with no hunks, no query ran, and
detect_changes reported `changed_files:1, changed_count:0, risk_level:'low'` —
"No changes detected." for a commit that deleted a function. A unified diff
spells an empty range as the line before it, so the anchor is line N alone:
a symbol containing the deleted text also contains N, while extending to N+1
would claim a symbol that merely starts after the gap — the widening
coalesceHunks guarantees it never does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* docs: say that a partial or truncated detect_changes is not a clean gate (#2915)

The gate itself now fails loudly, but the instructions every agent reads still
described a zero as a result. Fixed at the source: AGENTS.md's gitnexus block is
generated from a template in cli/ai-context.ts and injected into every user's
repo, so the sentence goes there and AGENTS.md/CLAUDE.md are regenerated through
the real code path (which also picks up a pre-existing `analyze --index-only`
drift the committed docs were behind).

That block is under a test-enforced size cap with 30 characters of headroom, so
the 144-character clause was paid for in the same currency: the header
exhortation, which the Always Do list restates as MUSTs with commands, and a
verbatim repeat of the detect-changes command in the regression-compare example.
3549 of 3552. Worth noting for whoever adds the next line — #2899 replaced an
absolute cap with a 0.65 ratio to let "a legitimate clause fit without
ceremony", but set the ratio flush against the block's then-current size, so it
is a ratchet with no ratchet.

The canonical block does not make the skills redundant: three of the four
install channels ship skills without touching AGENTS.md, --skip-agents-md does
the same in-repo, and a user-trimmed gitnexus:keep block legitimately has no
Always Do section — in those repos the skill file is the only carrier. Precedent
agrees: the risk:UNKNOWN rule is deliberately carried in both places. So one
sentence each in gitnexus-work (the commit gate), gitnexus-impact-analysis
(beside the UNKNOWN paragraph) and gitnexus-refactoring, whose post-hoc "verify
only expected files changed" is the worst of the three because a degraded result
makes it vacuously pass. gitnexus-taint-analysis is left alone: its audience is
always inside this repo, where the canonical block loads.

All copies mirrored to npm, plugin and cursor. The cursor copies are condensed
checklists rather than byte-mirrors, so they carry the equivalent note placed
where it governs every detect_changes line in the file — and nothing tests that,
since standard skills are fragment-checked rather than byte-compared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* refactor: break the seven small import cycles gitnexus check reports (#2915)

`check` reported 11 cycles. Five are paths inside a single 257-file strongly
connected component in core/ingestion (call-extractors / cfg visitors /
utils/ast-helpers), with a second 26-file component behind it — fixing those
paths would only make check print different ones, so both are left for their own
PR. This closes the seven that are genuinely separable, taking the graph from 9
strongly connected components to 2.

Six of the seven were one value import plus one `import type` edge. tsconfig
sets neither verbatimModuleSyntax nor isolatedModules, so those edges erase
entirely — the cut is a graph and readability change with no emitted-JS
difference. Each moved type went to a leaf module, with a re-export left behind
only where an importer outside the change actually needed it:

- cli/ai-context <-> cli/skill-gen: GeneratedSkillInfo -> cli/generated-skill.ts.
  One importer, no package export surface, so a clean move with no re-export.
- cli/analyze-config <-> cli/analyze (+core/run-analyze): AnalyzeOptions ->
  cli/analyze-options.ts. Re-export kept because a test imports it from
  analyze.js. run-analyze needed no edit — cutting the one type edge collapses
  the 3-file component into a DAG. Its own same-named AnalyzeOptions is a
  different interface and was deliberately not merged.
- ingestion/import-resolvers/types <-> ingestion/language-config: type-only in
  BOTH directions, so it had no runtime existence at all. ImportConfigs has no
  importers outside the pair and is the return type of loadImportConfigs, so it
  moved into language-config. Side effect worth having: the shared resolver
  types module no longer names a single language, which is an AGENTS.md rule for
  core/ingestion shared pipeline code.
- ingestion/di-extractors barrel <-> spring: DiResolver and the two match types
  -> di-extractors/types.ts, following the import-resolvers/types.ts precedent.
- scope-resolution/walkers <-> workspace-index: WorkspaceResolutionIndex ->
  workspace-index-types.ts. Re-export is load-bearing — 9 src importers, 4 test
  files, and a dynamic import() at contract/scope-resolver.ts. Moving the value
  isClassLike instead was rejected: ~15 value importers, and it is documented as
  a pair with isShapeLike.
- server/analyze-worker <-> analyze-worker-core: the WorkerMessage protocol ->
  analyze-worker-protocol.ts, a declarations-only leaf.

storage/branch-index <-> storage/repo-manager was the one genuine two-way
runtime cycle: branch-index called getStoragePaths/loadMeta, repo-manager used
branchSlug/BRANCHES_DIR. branch-index's header conceded the cycle and argued it
was ESM-safe because neither side calls across at module-evaluation time — a
guarantee resting on call ordering rather than structure. Folding
resolveBranchPlacement back the other way does not help, because
BranchSummary.stats is typed RepoMeta['stats'], so RepoMeta had to move either
way. Extracted storage/repo-meta.ts, a leaf importing only fs and path, holding
the metadata read primitives; repo-manager re-exports the public names so all
54 RepoMeta and 50 loadMeta importers are untouched. The moved block diffs
byte-identical against HEAD.

Verified beyond typecheck, because the worker entrypoint is the risky part and
nothing in the suite forks it: emitted analyze-worker.js still contains exactly
one runtime import, and forking the real worker over IPC boots it through
entry -> core -> protocol -> terminal-claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* refactor: apply the reuse, simplification, efficiency and altitude cleanups (#2915)

The one that mattered: the degradation exit code was fixed at the wrong depth.
`output()` has never inspected `partial` — it tests `error` only — so putting
the check in `detectChangesCommand` left every other tool exiting 0 on a
degraded run. `partial` is cross-tool vocabulary: query (enrichmentDegraded ||
ftsPartial), impact (!traversalComplete, perSymbolEnrichmentCapped) and the
mode:'pdg' envelope all emit it. A truncated impact traversal returns a short
caller set and an under-ranked risk, then exits 0 — so `gitnexus impact … &&
<edit>` proceeds, in the tool AGENTS.md makes a MUST gate before every edit.
The justification also cited checkCommand as precedent, but checkCommand passes
STRINGS too — it was the second command already hand-rolling around this gap,
while output()'s docstring called itself "the one place that keeps scripted
callers honest". output() now takes an optional renderer and fails on error OR
partial; two hand-rolled sites go away and three tools are covered instead of
one. truncated stays exit 0 (only the listing is capped) and checkCommand's
cycleCount policy stays put.

Efficiency, all re-measured on the 25k-node index:
- The process lookup was chunked with LBUG_QUERY_BATCH_SIZE, calibrated for the
  opposite query shape — that constant is for a whole-node-table scan where more
  items amortise the scan, while this is an `id IN $ids` probe where round trips
  dominate. 20k ids: 617ms at 100, 261ms at 1000. New LBUG_ID_PROBE_BATCH_SIZE,
  documented against its sibling so they cannot be re-merged. This also settles
  the older "chunking this query is a regression" measurement — that was chunk=100.
- The sort comparator re-coerced fields ChangedSymbolRow already types, O(n log n)
  redundant conversions (+31-38%). Row shape probed directly: alias-keyed, no
  positional keys, numeric columns are JS numbers.
- exactlyMatchedPaths built two throwaway arrays; one loop instead (40k rows
  11.4ms -> 4.5ms).
- The integration fixture seeded 710 step edges one round trip at a time; one
  UNWIND instead. File wall time 6.91s -> 3.63s. Fixture size unchanged — its
  docstring records the threshold below which the bug stops reproducing, and the
  mutation check still fails 3/3 when ORDER BY step is reverted.

Reuse and simplification:
- CALL_EDGE_LIMIT existed in four places; its own docstring predicted the drift
  it then caused. prompts.ts owns it now — it is a zero-import leaf so the
  direction cannot cycle, and had graph-queries.ts owned it the four suites that
  vi.mock that module would have left slice(0, undefined), silently returning
  every edge in exactly the tests meant to police the cap.
- Six dead positional row fallbacks survived the rewrite in the loop this branch
  re-indented, in the same PR that deleted the identical ABI from graph-queries.ts.
- Two test files independently modelled the same labels() scalar-string quirk.
  Deleted the wiki one — the file's own new header says semantics belong in the
  real-engine test — and kept projectTypeColumn, the only instrument that can see
  the bug for the detect_changes query.
- makeRepo onto the shared git bootstrap (the eleventh copy of the sequence the
  helper was extracted to own), the duplicate diff-args unwrapper merged into
  test/helpers, hand-rolled comparators onto compareCodeUnits, real-timer sleeps
  replaced by wave-released promises with a strengthened per-wave assertion.
- Re-exports trimmed to what is actually imported, a cross-reference this branch
  invalidated by moving mapConcurrent, and a "~20% faster" claim that does not
  survive at real index sizes (1-9%; the 10x memory win does).

Also adds the drift guard the new doc text lacked: fragment coverage for the
partial/truncated paragraph in every skill copy and in the managed AGENTS.md /
CLAUDE.md block. Falsifiability checked — none of those fragments exist at the
merge base.

Not done here, deliberately: 27 live labels(x)[0] projections remain across
impact/context/query/trace and MCP resources, with four load-bearing workarounds
that have begun depending on each other and one that fabricates rather than
degrades. That is a semantic change to five agent-facing tools and wants its own
PR, scoped to delete the workarounds too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* fix(cli): restore the detect-changes subcommand in the regression example (#2915)

Caught by the gitnexus-check bot on the PR. The regression-review fallback in
the injected mandate rendered as `--scope compare --base-ref "main" --repo .`
with no command, so anyone copying it invokes the runner with an option as its
first argument.

Self-inflicted, and by exactly the mechanism flagged when it landed: the block
is under a test-enforced size cap (#856) that had 30 characters of headroom, so
adding the partial/truncated clause required paying for it, and the 38-character
"repeat" that was dropped turned out to be the subcommand rather than a repeat.

Paid for the restoration out of the clause instead — both parentheticals are
gone, since `partial` and `truncated` are already defined in the tool
description this text points at. Block is back under the cap at 3548/3552.

Notably the cap has now been raised four times (2700 -> 2900 -> 2950, then
0.55 -> 0.65) each with the argument that the new line is load-bearing, and it
has now also caused a user-facing defect. It is not functioning as a budget.
Left at 0.65 here rather than making it five: moving the threshold to fit one's
own text is how it got here. Worth restructuring separately.

The fragment guard added a commit ago caught the rewording immediately, which
is what it is for; its fragments now pin the two policy claims rather than the
prose around them, since that prose is what gets re-trimmed under the cap.

Also verified and NOT changed: the bot's other error, that detect_changes
compares 1-based hunks against 0-based graph lines. `bounds` is built from
`coalesceHunksByPath`, which applies `toZeroBasedLine` to both ends at the
grouping boundary, and both a mocked and a real-engine test pin an edit landing
on a symbol's last line. The bot read `parseDiffHunks` in isolation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* fix(core): reject a fractional chunk size, and stop the truncation note overclaiming (#2915)

All five from the gitnexus-check bot's pass on the previous push; two were
introduced by the cleanup round that preceded it.

`chunk` guarded with `Number.isFinite`, which admits a fractional size — and
that one does not fail, it DUPLICATES. `slice` truncates its indices while `i`
does not, so size 1.5 yields slice(0, 1.5) = items 0-1 then slice(1.5, 3) =
items 1-2, putting item 1 in two batches; a caller batching a query would send
it twice. A size is a count, so `Number.isInteger`. Unreachable today (every
caller passes a constant) but the guard existed precisely for the unreachable
case, and the NaN half of it was already there.

`mapConcurrent`'s per-item degradation contract had a hole: `onError` is
caller-supplied and was invoked outside a try, so a throwing reporter rejected
`settle`, rejected the whole `Promise.all` wave, and discarded the neighbouring
successes the function exists to preserve. Reporting a failure must not become
one.

The CLI truncation note asserted "the counts and risk level still cover all of
them", which is true only when `truncated` fires alone — with `partial` the
counts are summed from the batches that succeeded. It now varies: a distinct
string when both flags are set, saying the counts are a lower bound. This is the
same claim already corrected in the tool description; the CLI text still had the
old one.

The di-extractors contract docstring claimed the barrel re-exports everything
from it. That stopped being true when the re-export was trimmed to what is
actually imported, one commit earlier.

The real-engine wiki test claimed to prepare "every exported query" and omitted
`getInterModuleEdgesForOverview`, which `generateOverview` calls. Added — it
aggregates in JS over `getInterFileCallEdges` rather than issuing its own
Cypher, so the note says why it is in a prepare test.

Verified and NOT changed: the bot's other error, that detect_changes compares
1-based hunks against 0-based graph lines. `bounds` is built from
`coalesceHunksByPath`, which converts both ends at the grouping boundary
(storage/git.ts), and two tests pin an edit landing on a symbol's last line.
The remaining seven findings are changed-symbol heads-ups with no signature
change; their callers' suites are green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

* fix(mcp): make the IMPACT_MAX_CHUNKS fallback actually fire (#2915)

The validation added earlier this branch used `Number.parseInt`, which takes the
numeric PREFIX: '1.5' parses to 1, satisfies `Number.isInteger`, and silently
caps enrichment after a single 100-item batch — the opposite of the fallback the
comment beside it promised. `Number` instead, so a fractional value is rejected
and falls back to 10.

The emptiness check is load-bearing rather than defensive: `Number('')` is 0 and
0 is a legitimate value here (enrich nothing), so an UNSET variable would
otherwise mean "enrich nothing" rather than "use the default".

Behaviour table, old vs new: '1.5' 1 -> 10 (the bug), and undefined/''/'  '/
'10junk'/'-2'/'all' -> 10, '0' -> 0, '3' -> 3, ' 5 ' -> 5 all unchanged. So the
only case that moves is the reported one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139ycbqnAorkJGQaZQXUNuv

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:51:17 +01:00
..
.claude feat(cpp): sfinae filter (#1623) 2026-05-16 20:23:13 +01:00
bench fix(scope-resolution): resolve a package whose directory name repeats higher in the path (#2881) (#2929) 2026-08-12 10:26:39 +01:00
hooks fix(cli): stop churning the committed agent guides, and nudge --index-only (#2907) (#2927) 2026-08-11 13:30:14 +01:00
scripts fix(skills): publish gitnexus-plan artifacts on macOS without an interpreter (#2905) (#2922) 2026-08-11 12:24:53 +01:00
skills fix(mcp): stop scaling the detect_changes query with the diff's hunk count (#2915) (#2930) 2026-08-12 14:51:17 +01:00
src fix(mcp): stop scaling the detect_changes query with the diff's hunk count (#2915) (#2930) 2026-08-12 14:51:17 +01:00
test fix(mcp): stop scaling the detect_changes query with the diff's hunk count (#2915) (#2930) 2026-08-12 14:51:17 +01:00
vendor perf(communities): fix the O(communities x N) copy in vendored Leiden, wire Icebug to its real API (#2337) (#2692) 2026-07-25 13:23:17 +01:00
.env.example feat(serve): validate and port-scope the origin/proxy configuration surface (#2820) 2026-08-05 06:52:39 +01:00
.npmignore feat(install): toolchain-free tree-sitter via vendored prebuilds (#2113) 2026-06-09 18:16:24 +01:00
CHANGELOG.md chore: release v1.6.9 (#2367) 2026-07-04 07:53:06 +01:00
Dockerfile.test fix(security): Pin Docker Node base images, remove runtime package-manager CVE surface, verify Trivy on PRs, and harden Dependabot policy (#1455) 2026-05-09 16:55:31 +01:00
package-lock.json chore(deps)(deps-dev): bump tsx from 4.23.5 to 4.23.11 in /gitnexus (#2925) 2026-08-11 11:48:00 +00:00
package.json chore(deps)(deps): bump @ladybugdb/core in /gitnexus (#2863) 2026-08-08 08:17:00 +01:00
README.md feat: make MAX_CALLABLE_VALUE_TARGETS configurable via env (#2725) 2026-08-01 12:42:36 +01:00
tsconfig.json perf(cfg): SSA-sparse reaching-defs to replace the dense-set worklist (#2201) (#2212) 2026-06-15 19:16:53 +01:00
tsconfig.test.json test: add test suite with vitest (unit + integration + fixtures) 2026-03-01 20:07:02 +05:30
vitest.config.ts fix(mcp): stop scaling the detect_changes query with the diff's hunk count (#2915) (#2930) 2026-08-12 14:51:17 +01:00

GitNexus

Graph-powered code intelligence for AI agents. Index any codebase into a knowledge graph, then query it via MCP or CLI.

Works with Cursor, Claude Code, Antigravity (Google), Codex, Windsurf, Cline, OpenCode, CodeBuddy (Tencent), Qoder (Alibaba), and any MCP-compatible tool.

npm version License: PolyForm Noncommercial


Why?

AI coding tools don't understand your codebase structure. They edit a function without knowing 47 other functions depend on it. GitNexus fixes this by precomputing every dependency, call chain, and relationship into a queryable graph.

Three commands to give your AI agent full codebase awareness.

Quick Start

# Index your repo (run from repo root)
npx gitnexus analyze

That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates AGENTS.md / CLAUDE.md context files — all in one command.

On npm 11.x? npx can crash during install (Cannot destructure property 'package' of 'node.target'). Use the pnpm form instead:

pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze

See Troubleshooting → npx gitnexus crashes with node.target is null (npm 11) for the full matrix (global install, npm downgrade).

To configure MCP for your editor, run npx gitnexus setup once — or set it up manually below.

gitnexus setup auto-detects your editors and writes the correct global MCP config. You only need to run it once. To configure only selected integrations, pass --coding-agent/-c with a comma-separated list or repeat the option, for example gitnexus setup -c cursor,codex.

Editor Support

Editor MCP Skills Hooks (auto-augment) Support
Claude Code Yes Yes Yes (PreToolUse + PostToolUse) Full
Cursor Yes Yes Yes (postToolUse, manual install) Full
Antigravity (Google) Yes Yes Yes (AfterTool, Gemini CLI hooks schema) Full
Codex Yes Yes Yes (PreToolUse + PostToolUse, Codex hooks) Full
OpenCode Yes Yes — MCP + Skills
CodeBuddy (Tencent) Yes Yes — MCP + Skills
Qoder (Alibaba) Yes Yes — MCP + Skills
Windsurf Yes — — MCP

Claude Code and Codex get the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.

Community Integrations

Agent Install Source
pi pi install npm:pi-gitnexus pi-gitnexus

MCP Setup (manual)

If you prefer to configure manually instead of using gitnexus setup:

Claude Code (full support — MCP + skills + hooks)

# macOS / Linux
claude mcp add gitnexus -- npx -y gitnexus@latest mcp

# Windows
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp

Codex (full support — MCP + skills + hooks)

codex mcp add gitnexus -- npx -y gitnexus@latest mcp

Codex hooks (PreToolUse graph enrichment + PostToolUse stale-index detection in ~/.codex/hooks.json, same schema as Claude Code) need the bundled adapter script, so they are installed by gitnexus setup -c codex rather than manually.

Alternatively, install everything as a Codex plugin (MCP + skills + hooks in one step):

codex plugin marketplace add abhigyanpatwari/GitNexus
# then inside Codex: /plugins → install "GitNexus"

Codex notes: SessionStart is intentionally not registered — Codex reads AGENTS.md natively, which already carries the GitNexus context block. Newly installed hooks need a one-time approval in Codex via /hooks before they run. Pick one install route (gitnexus setup -c codex or the plugin): plugin hooks load alongside ~/.codex/hooks.json, so installing both can fire duplicate hooks per tool call.

Cursor / Windsurf

Add to ~/.cursor/mcp.json (global — works for all projects):

{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

OpenCode

Add to ~/.config/opencode/config.json:

{
  "mcp": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

CodeBuddy

CodeBuddy reads only the first existing file in its config priority chain: ~/.codebuddy/.mcp.json (recommended) → ~/.codebuddy/mcp.json (deprecated) → ~/.codebuddy.json (legacy). Edit the first non-empty file that exists — creating a higher-priority file would hide the servers in the ones below it. If none exist, create ~/.codebuddy/.mcp.json:

{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

Qoder

Add to ~/.qoder.json:

{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

How It Works

GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:

  1. Structure — Walks the file tree and maps folder/file relationships
  2. Parsing — Extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
  3. Resolution — Resolves imports and function calls across files with language-aware logic
    • Field & Property Type Resolution — Tracks field types across classes and interfaces for deep chain resolution (e.g., user.address.city.getName())
    • Return-Type-Aware Variable Binding — Infers variable types from function return types, enabling accurate call-result binding
  4. Clustering — Groups related symbols into functional communities
  5. Processes — Traces execution flows from entry points through call chains
  6. Search — Builds hybrid search indexes for fast retrieval

The result is a LadybugDB graph database stored locally in .gitnexus/ with full-text search and semantic embeddings.

Experimental community detection engine

Experimental — not supported for production indexes. The Icebug engine is a research path for #2337. It carries no stability guarantee, may change or be removed without a major version, and partitions differently from the default, so switching engines changes community IDs and any generated context keyed on them. Reindex with graphology before relying on the output.

Community detection uses the bundled Graphology Leiden implementation by default. To try the #2337 Icebug path without changing default analyze behavior, install the optional native package alongside GitNexus and set the engine:

npm i @ladybugmem/icebug
GITNEXUS_COMMUNITY_ENGINE=icebug npx gitnexus analyze

Supported values are graphology, icebug, and auto. Today auto is behaviorally identical to icebug: both try Icebug and fall back to Graphology, while graphology skips Icebug entirely.

Icebug is not a declared dependency — its prebuilds link against system Arrow 24 (libarrow.so.2400), OpenMP, and glibc ≥ 2.38, none of which GitNexus can assume. Analyze falls back to Graphology and reports the reason in progress output when the module is missing, fails to load, or predates the setNumberOfThreads / setSeed controls that reproducible community IDs require (present at icebug-nodejs HEAD, absent from the published 12.8.0 tarball — so the fallback is what you will see today). The engine is pinned to threads: 1, randomize: false for determinism.

Note that the bundled Graphology path is no longer the slow option it once was: #2337 removed an accidental O(communities × N) copy in the vendored Leiden. On a synthetic 200k-node / 800k-edge benchmark graph it went from exceeding the 60s timeout to finishing in ~15s. Real projections vary with their degree distribution, so treat that as a direction, not a guarantee.

MCP Tools

Your AI agent gets 17 tools (15 per-repo + 2 group) automatically:

Tool What It Does
list_repos Discover all indexed repositories (paginated — limit/offset)
query Process-grouped hybrid search (BM25 + semantic + RRF)
context 360-degree symbol view — categorized refs, process participation
impact Blast radius analysis with depth grouping and confidence
trace Shortest directed path between two symbols (call + class-member edges)
detect_changes Git-diff impact — maps changed lines to affected processes
check Read-only structural checks against the indexed graph
rename Multi-file coordinated rename with graph + text search
cypher Raw Cypher graph queries
route_map API route map — which components fetch which endpoints, and handlers
tool_map MCP/RPC tool definitions — where they're defined and handled
shape_check Validate API response shapes against consumers' property accesses
api_impact Pre-change impact report for an API route handler
explain Explain persisted taint findings (source→sink flows, --pdg indexes)
pdg_query Query control/data dependence at statement level (--pdg indexes)
group_list List configured repository groups
group_sync Rebuild a group's Contract Registry and cross-repo links

With one indexed repo, the repo param is optional. With multiple, specify which: query({search_query: "auth", repo: "my-app"}). Per-repo tools also take an optional branch for indexes pinned with gitnexus analyze --branch; omitting it queries the workspace index, which follows your checked-out working tree. explain and pdg_query need an index built with gitnexus analyze --pdg.

MCP Resources

Resource Purpose
gitnexus://repos List all indexed repositories (read first)
gitnexus://setup Setup and usage guidance for agents
gitnexus://repo/{name}/context Codebase stats, staleness check, and available tools
gitnexus://repo/{name}/clusters All functional clusters with cohesion scores
gitnexus://repo/{name}/cluster/{name} Cluster members and details
gitnexus://repo/{name}/processes All execution flows
gitnexus://repo/{name}/process/{name} Full process trace with steps
gitnexus://repo/{name}/schema Graph schema for Cypher queries
gitnexus://group/{name}/contracts A group's extracted contracts and cross-links
gitnexus://group/{name}/status Staleness of repos in a group

MCP Prompts

Prompt What It Does
detect_impact Pre-commit change analysis — scope, affected processes, risk level
generate_map Architecture documentation from the knowledge graph with mermaid diagrams

CLI Commands

gitnexus setup                   # Configure MCP for detected editors (one-time; use -c to select)
gitnexus uninstall               # Preview removal of GitNexus MCP/skills/hooks (add --force to apply)
gitnexus analyze [path]          # Index a repository (or update stale index)
gitnexus analyze --repair-fts    # Fast path: rebuild/verify only FTS indexes on existing index data
gitnexus analyze --force         # Full rebuild: re-parse + graph rebuild + FTS rebuild
gitnexus analyze --embeddings    # Enable embedding generation (slower, better search)
gitnexus embeddings install      # Fetch the optional local embedding stack on demand (--cuda, --force)
gitnexus analyze --skills        # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-agents-md  # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-skills   # Skip installing standard .claude/skills/gitnexus-* skill files
gitnexus analyze --skip-git      # Index folders that are not Git repositories
gitnexus analyze --workers <n>   # Parse worker pool size (>=1; default: cores-1, capped at 16)
gitnexus analyze --verbose       # Log skipped files when parsers are unavailable
gitnexus analyze --max-file-size 1024  # Skip files larger than N KB (default: 512, cap: 32768)
gitnexus analyze --worker-timeout 60  # Increase worker idle timeout for slow parses
gitnexus analyze --wal-checkpoint-threshold 67108864  # 64 MiB. Control LadybugDB WAL auto-checkpoint threshold (default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
gitnexus mcp                     # Start MCP server (stdio) — serves all indexed repos
gitnexus serve                   # Start local HTTP server (multi-repo) for web UI
gitnexus index                   # Register an existing .gitnexus/ folder into the global registry
gitnexus list                    # List all indexed repositories
gitnexus status                  # Show index status for current repo
gitnexus clean                   # Delete index for current repo
gitnexus clean --all --force     # Delete all indexes
gitnexus wiki [path]             # Generate LLM-powered docs from knowledge graph
gitnexus wiki --model <model>    # Wiki with custom LLM model (default: minimax/minimax-m2.5)
gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
                                  # Allow an exact LAN/self-hosted HTTP LLM host; env: GITNEXUS_ALLOW_INSECURE_CONNECTION
gitnexus doctor                  # Show runtime platform capabilities and embedding configuration

# Direct graph queries — the same tools the MCP server exposes, no MCP daemon needed
gitnexus query "<concept>"                                    # Process-grouped hybrid search
gitnexus context <symbol> [--uid <uid> | --file <path>]       # 360° symbol view; flags disambiguate a shared name
gitnexus impact <symbol> [--uid <uid> | --file <path> | --kind <kind>]  # Blast radius; flags disambiguate a shared name
gitnexus trace <from> <to>       # Shortest directed path between two symbols
gitnexus detect-changes          # Map the working-tree diff to affected symbols and execution flows
gitnexus check                   # Read-only structural checks against the indexed graph
gitnexus cypher "<query>"        # Run a raw Cypher query against the knowledge graph

# Repository groups (multi-repo / monorepo service tracking)
gitnexus group create <name>                                   # Create a repository group
gitnexus group add <group> <groupPath> <registryName>          # Add a repo to a group. <groupPath> is a hierarchy path (e.g. hr/hiring/backend); <registryName> is the repo's name from the registry (see `gitnexus list`)
gitnexus group remove <group> <groupPath>                      # Remove a repo from a group by its hierarchy path
gitnexus group list [name]                                     # List groups, or show one group's config
gitnexus group sync <name>                                     # Extract contracts and match across repos/services
gitnexus group contracts <name>  # Inspect extracted contracts and cross-links
gitnexus group query <name> <q>  # Search execution flows across all repos in a group
gitnexus group status <name>     # Check staleness of repos in a group
gitnexus group impact <name> --target <symbol> --repo <groupPath>  # Cross-repo blast radius

gitnexus uninstall reverses gitnexus setup — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified by bundled gitnexus skill name (e.g. gitnexus-cli/), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass --force to apply. Per-repo indexes (gitnexus clean --all) and the global npm package (npm uninstall -g gitnexus) are left for you to remove.

Remote Embeddings

Set these env vars to use a remote OpenAI-compatible /v1/embeddings endpoint instead of the local model:

export GITNEXUS_EMBEDDING_URL=http://your-server:8080/v1
export GITNEXUS_EMBEDDING_MODEL=BAAI/bge-large-en-v1.5
export GITNEXUS_EMBEDDING_DIMS=1024          # optional, default 384
export GITNEXUS_EMBEDDING_REQUEST_DIMS=omit  # optional: omit "dimensions", or an integer to override it
export GITNEXUS_EMBEDDING_API_KEY=your-key   # optional, default: "unused"
export GITNEXUS_EMBEDDING_MAX_ATTEMPTS=3     # optional, total attempts (1-20)
export GITNEXUS_EMBEDDING_RETRY_CAP_MS=5000  # optional, maximum retry delay
export GITNEXUS_EMBEDDING_MIN_INTERVAL_MS=0  # optional, minimum request spacing
export GITNEXUS_EMBEDDING_HTTP_TIMEOUT_MS=180000 # optional, per-request timeout (max 300000)
gitnexus analyze . --embeddings

GITNEXUS_EMBEDDING_REQUEST_DIMS controls only the dimensions field sent in the request body, independently of GITNEXUS_EMBEDDING_DIMS (which still validates the returned vector's length):

  • omit (or none, off, false, 0) — do not send dimensions at all, for strict backends that return the right vector size but reject the field.
  • a positive integer — send that value instead of GITNEXUS_EMBEDDING_DIMS.
  • unset — send GITNEXUS_EMBEDDING_DIMS (the previous behavior).

Works with Infinity, vLLM, TEI, llama.cpp, Ollama, LM Studio, or OpenAI. Retry and pacing settings are provider-neutral; provider-specific limits should be supplied through configuration. When unset, local embeddings are used unchanged.

JVM Package Sibling Injection

Java and Kotlin files in the same package receive implicit sibling class bindings to resolve same-package references. By default, GitNexus injects at most 200 siblings per module scope, nearest first by path. Set GITNEXUS_MAX_INJECTED_SIBLINGS=0 to remove that per-file limit; this can substantially increase indexing work for large packages.

export GITNEXUS_MAX_INJECTED_SIBLINGS=200
gitnexus analyze .

When the limit truncates a file's sibling set, that file is marked visibility-incomplete: same-package references still resolve through the injected siblings, but wildcard-import attribution (used by the Spring bean/DI/config passes) is disabled for it rather than resolved against a partial view. Analyze logs a sibling injection truncated warning naming how many files were affected.

Packages with more than 500 files are a separate, fixed limit: they are skipped entirely (logged as skipping package with N files) and every file in them is marked visibility-incomplete. GITNEXUS_MAX_INJECTED_SIBLINGS does not lift that skip — including at 0.

Multi-Repo Support

GitNexus supports indexing multiple repositories. Each gitnexus analyze registers the repo in a global registry (~/.gitnexus/registry.json). The MCP server serves all indexed repos automatically.

Supported Languages

TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart

Language Feature Matrix

Language Imports Named Bindings Exports Heritage Type Annotations Constructor Inference Config Frameworks Entry Points
TypeScript ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
JavaScript ✓ ✓ ✓ ✓ — ✓ ✓ ✓ ✓
Python ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Java ✓ ✓ ✓ ✓ ✓ ✓ — ✓ ✓
Kotlin ✓ ✓ ✓ ✓ ✓ ✓ — ✓ ✓
C# ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Go ✓ — ✓ ✓ ✓ ✓ ✓ ✓ ✓
Rust ✓ ✓ ✓ ✓ ✓ ✓ — ✓ ✓
PHP ✓ ✓ ✓ — ✓ ✓ ✓ ✓ ✓
Ruby ✓ — ✓ ✓ — ✓ — ✓ ✓
Swift — — ✓ ✓ ✓ ✓ ✓ ✓ ✓
C — — ✓ — ✓ ✓ — ✓ ✓
C++ — — ✓ ✓ ✓ ✓ — ✓ ✓
Dart ✓ — ✓ ✓ ✓ ✓ — ✓ ✓

Imports — cross-file import resolution · Named Bindings — import { X as Y } / re-export tracking · Exports — public/exported symbol detection · Heritage — class inheritance, interfaces, mixins · Type Annotations — explicit type extraction for receiver resolution · Constructor Inference — infer receiver type from constructor calls (self/this resolution included for all languages) · Config — language toolchain config parsing (tsconfig, go.mod, etc.) · Frameworks — AST-based framework pattern detection · Entry Points — entry point scoring heuristics

Agent Skills

GitNexus ships with skill files that teach AI agents how to use the tools effectively:

  • Exploring — Navigate unfamiliar code using the knowledge graph
  • Debugging — Trace bugs through call chains
  • Impact Analysis — Analyze blast radius before changes
  • Refactoring — Plan safe refactors using dependency mapping
  • Guide — GitNexus tool/resource/schema reference for the agent
  • CLI — Run analyze/status/clean/wiki commands on request
  • PDG Query — Statement-level control/data dependence queries (--pdg index)
  • Taint Analysis — Source→sink data-flow findings (--pdg index)
  • Plan / Work / Review / LFG — The engineering family: implementation-ready plans, gated plan execution, graph-backed change review with taint + expert lenses, and the end-to-end pipeline

Installed automatically by both gitnexus analyze (per-repo) and gitnexus setup (global). Run gitnexus analyze --skills to additionally generate each detected functional area as a direct project skill under .claude/skills/gitnexus-area-<name>/.

Requirements

  • Node.js >= 22
  • Git repository (uses git for commit tracking)
  • Linux: glibc 2.34 or newer (Ubuntu 22.04+, RHEL/Rocky/Alma 9+, Debian 12+, Fedora 35+). The LadybugDB native binary ships as a prebuild against that floor, so on an older host it cannot load and reinstalling does not help — see Linux: GLIBC_2.34' not found.
  • Windows, for full-text search: the Microsoft Visual C++ 2015-2022 Redistributable (x64) and OpenSSL 3 (libssl-3-x64.dll, libcrypto-3-x64.dll) resolvable on PATH — see Windows: full-text search unavailable.

Release candidates

Stable releases publish to the default latest dist-tag. When a pull request with non-documentation changes merges into main, an automated workflow also publishes a prerelease build under the rc dist-tag, so early adopters can try in-flight fixes without waiting for the next stable cut. (Docs-only merges are skipped.)

# Try the latest release candidate (pre-stable — may change at any time)
npm install -g gitnexus@rc
# — or —
npx gitnexus@rc analyze

Release-candidate versions follow the standard semver prerelease format X.Y.Z-rc.N, where X.Y.Z is the next stable target (bumped from the current latest by patch by default; minor or major when kicking off a bigger cycle) and N increments per published rc. Example sequence: 1.6.2-rc.1, 1.6.2-rc.2, …, then once 1.6.2 ships stable, 1.6.3-rc.1. See the Releases page for the full list; stable latest is unaffected.

Troubleshooting

Cannot destructure property 'package' of 'node.target' as it is null

This error comes from npm 11.x's arborist while installing gitnexus (often via npx), before gitnexus code runs. It is triggered by platform-filtered optionalDependencies in native packages such as onnxruntime-node / @huggingface/transformers (used when indexing with --embeddings). GitNexus cannot catch it at runtime — use one of these workarounds:

pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze       # auto-selected when pnpm + npm 11+
npm install -g gitnexus@latest         # global install avoids per-run npx reify
gitnexus analyze                       # if already installed globally

On pnpm 10+, lifecycle scripts are blocked unless explicitly allowed — the resolver adds --allow-build for @ladybugdb/core, gitnexus, and tree-sitter automatically when it picks pnpm dlx.

If you must stay on npm 11.x without pnpm, downgrade npm toolchain-wide (last resort):

npm install -g npm@10.9.0

See #1939 and the original #819 thread. An older variant of this crash (tree-sitter-dart tarball URL) was fixed in gitnexus v1.6.2+ (#820); if you still see install failures after upgrading, clear cache:

npm cache clean --force
npx gitnexus@latest analyze

ERR_DLOPEN_FAILED / lbugjs.node missing (pnpm dlx, pnpx)

GitNexus depends on @ladybugdb/core, whose native database addon (lbugjs.node) is placed by a postinstall script. pnpm dlx, pnpx, and any install run with --ignore-scripts skip lifecycle scripts, so the addon is never put in place and the runtime crashes with ERR_DLOPEN_FAILED:

Error: dlopen(.../@ladybugdb/core/lbugjs.node, ...): tried: '...' (no such file)
  code: 'ERR_DLOPEN_FAILED'

Options that run install scripts:

# pnpm dlx with explicit build permission (one-off, no global install required)
pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter \
  dlx gitnexus@latest serve

# npm: global install (recommended on npm 11+; bare npx may crash — see section above)
npm install -g gitnexus@latest
gitnexus serve

# npx (npm < 11, or after upgrading npm)
npx gitnexus@latest serve

# pnpm: global install with build scripts allowed (pnpm 10.2+; no approve-builds -g on pnpm 11+)
pnpm add -g --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter gitnexus
gitnexus serve

Linux: GLIBC_2.34' not found

LadybugDB native binary (lbugjs.node) exists but failed to load:
  /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../lbugjs.node)

The LadybugDB addon ships as a prebuilt binary compiled against glibc 2.34. If your distribution is older (CentOS/RHEL 8 has 2.28, Ubuntu 20.04 has 2.31, Debian 11 has 2.31), the dynamic loader cannot resolve its symbols.

Reinstalling does not help — every download delivers the same prebuilt binary. The fix is a newer C library:

  • Run GitNexus on a distribution with glibc 2.34 or newer — Ubuntu 22.04+, RHEL/Rocky/Alma 9+, Debian 12+, Fedora 35+.
  • Or run it in the container image, which bundles a current glibc (see Docker).

gitnexus doctor reports the required and detected glibc versions when this happens (#2672).

Windows: full-text search unavailable

analyze completes, but keyword search is degraded and doctor shows the FTS extension failing with Windows error 126 (The specified module could not be found). The extension needs two runtime dependencies Windows does not ship by default:

  1. Microsoft Visual C++ 2015-2022 Redistributable (x64) — https://aka.ms/vs/17/release/vc_redist.x64.exe
  2. OpenSSL 3 — libssl-3-x64.dll and libcrypto-3-x64.dll, resolvable on PATH

The redistributable alone is not sufficient. If Git for Windows is installed you already have the OpenSSL DLLs — run gitnexus from Git Bash, or prepend the directory to PATH in the shell you use:

$env:PATH = "C:\Program Files\Git\mingw64\bin;$env:PATH"
gitnexus analyze --repair-fts

Without them the index is still built, but without search tables, so query returns empty keyword results until you re-run gitnexus analyze --repair-fts from a shell where the DLLs resolve (#2669).

Installation fails with native module errors

Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 before installing.

If npm install -g gitnexus fails on native modules:

# Ensure build tools are available (Linux/macOS)
# Ubuntu/Debian: sudo apt install python3 make g++
# macOS: xcode-select --install

# Retry installation
npm install -g gitnexus

Installation fails behind an HTTP proxy (onnxruntime-node postinstall)

onnxruntime-node's postinstall downloads optional CUDA GPU binaries from api.nuget.org — outside the npm registry, so registry mirrors don't cover it, and its proxy layer (global-agent) ignores the standard HTTP_PROXY/HTTPS_PROXY variables and rejects 302 redirects (#2370).

Since the packages are optional dependencies, a failed download no longer breaks npm install -g gitnexus — npm skips the embedding stack and everything else works. The stack then self-heals on demand: the first gitnexus analyze --embeddings (or an explicit gitnexus embeddings install) fetches it through your configured npm registry — mirrors and proxies apply, no NuGet download involved — into ~/.gitnexus/embedding-runtime.

# heal a proxy-degraded install manually (CPU embeddings; registry-only)
gitnexus embeddings install

# reinstall into the prefix even when the stack already resolves
gitnexus embeddings install --force

# CUDA GPU hosts: also fetch GPU binaries (NuGet; set the proxy global-agent reads)
GLOBAL_AGENT_HTTPS_PROXY=<proxy-url> gitnexus embeddings install --cuda

The prefix defaults to ~/.gitnexus/embedding-runtime; set GITNEXUS_EMBEDDING_RUNTIME_DIR to install it elsewhere (e.g. a writable path in a container).

Node requirement for the on-demand prefix: the self-heal loads the prefixed packages via module.registerHooks, available on Node ≥ 22.15 (on the 22.x line) or ≥ 23.5 (on the 23.x line). On an older Node the packages install but can't be loaded from the prefix — reinstall them into the install itself instead (works on every supported Node): ONNXRUNTIME_NODE_INSTALL=skip npm install -g gitnexus (Windows: set ONNXRUNTIME_NODE_INSTALL=skip && npm install -g gitnexus). Skipping only the CUDA download keeps full CPU embeddings (CPU embeddings don't need it). Check the result any time with gitnexus doctor (Embeddings → Support line).

Analyze warns about unavailable FTS or VECTOR extensions

GitNexus uses optional DuckDB extensions for BM25 and vector search. The gitnexus serve and MCP read paths only ever try to LOAD the extensions — they never block on a network install. The analyze command, by default, attempts one bounded out-of-process install if LOAD fails (a plain INSTALL to download a missing extension, escalating to FORCE INSTALL only when the LOAD error shows the existing file is broken or truncated, so a permanent non-file failure does not re-download on every run) and proceeds even when that install times out, so the index is always written to disk; BM25/vector search degrade gracefully until the extensions become available.

Configure the behavior with these environment variables:

Variable Values Default Effect
GITNEXUS_LBUG_EXTENSION_INSTALL auto, load-only, never auto auto runs one bounded install if LOAD fails — a plain INSTALL, escalating to FORCE INSTALL only when the LOAD error shows the present extension file is broken. load-only only uses already-installed extensions (recommended for offline / firewalled environments). never skips optional extensions entirely.
GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS positive integer 15000 Wall-clock budget for the out-of-process extension-install child before it is killed.
GITNEXUS_FTS_STEMMER supported LadybugDB stemmer porter Stemmer used when rebuilding BM25/FTS indexes. Use none for CJK-heavy repositories, or a language stemmer such as german, french, or spanish when that better matches repository comments and identifiers. Re-run gitnexus analyze --repair-fts after changing it.
GITNEXUS_FTS_CJK_SEGMENTATION none, bigram none bigram inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in content/description before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike GITNEXUS_FTS_STEMMER, this rewrites stored text — enabling it on an already-indexed repo requires a full gitnexus analyze --force; neither --repair-fts nor a plain incremental analyze applies it to previously-indexed files. Set the same value wherever analyze and search-serving processes (CLI query, MCP server, web server) run.
GITNEXUS_STREAM_GRAPH_EMIT 0, 1 1 (on) On by default on a full rebuild (--force); incremental runs ignore it. Holds structural relationships (CALLS, IMPORTS, ACCESSES, CONTAINS, ...) as CSV-on-disk plus compact in-memory columns instead of as objects in three overlapping indexes, cutting peak in-memory graph heap by ~1.4x at no measurable CPU cost (measured A/B on a synthetic 400k-node / 1.08M-edge graph: 819 MB -> 584 MB, iteration at parity, scaling verified linear from 100k to 800k nodes, with every edge still visible through the graph interface; no end-to-end measurement on a real repository yet). Nothing is traded away — community detection, process extraction, PDG taint summaries and the local-symbol pruner all read a complete relationship set and behave identically. Set to 0 only to bisect a suspected streaming-related fault.
GITNEXUS_COMMUNITY_ENGINE graphology, icebug, auto graphology Community-detection engine used during analyze. graphology is the supported default. icebug and auto are experimental and currently behave identically: both try the optional @ladybugmem/icebug native Leiden over a CSR export and fall back to Graphology if it is not installed, cannot load, or lacks the deterministic thread/seed controls. Experimental engines partition differently, so community IDs are not comparable across engines.
GITNEXUS_WAL_CHECKPOINT_THRESHOLD integer >= -1 67108864 (64 MiB) LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; -1 keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments.
GITNEXUS_LBUG_BUFFER_POOL_SIZE integer >= 0 (bytes) min(2 GiB, 80% RAM) LadybugDB buffer-pool ceiling for every GitNexus database (analyze, MCP server, serve, group bridges). Bounded so a long-lived gitnexus mcp process or a large incremental analyze cannot grow toward LadybugDB's native 80%-of-RAM default and OOM the host (#2557). 0 restores that native unbounded default; invalid values warn and fall back to the default. During analyze the pool is right-sized to the graph and, on non-4 KiB-page hosts (Apple Silicon 16 KiB, Ascend/aarch64 64 KiB), scaled by the page-size granule ratio up to min(2 GiB × pageSize/4 KiB, 80% RAM) (#2631); this env var overrides all of that as an absolute value.
GITNEXUS_LBUG_MAX_DB_SIZE positive integer (bytes) 17179869184 (16 GiB) Upper bound for a single LadybugDB database file. This is an mmap/disk-address-space ceiling, not a memory limit — it does not constrain the buffer pool (use GITNEXUS_LBUG_BUFFER_POOL_SIZE for that). Raise it when indexing genuinely huge monorepos; invalid values silently fall back to the default.
# Offline/airgapped: never reach the network for extensions
GITNEXUS_LBUG_EXTENSION_INSTALL=load-only npx gitnexus analyze

# Slow network: give extension downloads more time
GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS=30000 npx gitnexus analyze

# CJK-heavy codebase: rebuild keyword indexes without English stemming
GITNEXUS_FTS_STEMMER=none npx gitnexus analyze --repair-fts

# CJK-heavy codebase: enable sub-phrase search over Chinese/Japanese Han text.
# On an already-indexed repo, the first run after enabling this MUST be --force —
# --repair-fts and plain incremental `analyze` both leave old files un-segmented.
GITNEXUS_FTS_CJK_SEGMENTATION=bigram npx gitnexus analyze --force

Analysis runs out of memory

Memory management is automatic: analyze sizes its heap to the machine (always below physical RAM), caps each parse worker, and — rather than grinding into a GC death spiral or crash — stops early with a message telling you the one thing to do. Repeated Replacement worker did not report ready within 5000ms warnings on a large repository are part of the same picture: memory pressure starving healthy workers, not a worker bug (#2649).

If analyze says the repository doesn't fit, do what the message says:

  • The machine has more memory to give (a NODE_OPTIONS --max-old-space-size pin from your environment is holding analyze back): re-run without the pin — no flags needed.
  • The machine is the ceiling: shrink the scope (exclude generated or vendored directories, below) or use a machine with more RAM.

Escape hatches (GITNEXUS_MEMORY=off to decline the autopilot, GITNEXUS_WORKER_HEAP_MB to size workers yourself) are listed in the environment-variable table below — most users never need them.

For very large repositories:

# Increase Node.js heap size
NODE_OPTIONS="--max-old-space-size=16384" npx gitnexus analyze

# Exclude large directories (this repo only)
echo "vendor/" >> .gitnexusignore
echo "dist/" >> .gitnexusignore

# Exclude a directory across every repo you index, without touching each
# repo's own .gitnexusignore or needing push/commit access to it. GitNexus
# reads the same sources `git` itself does: core.excludesFile (all repos)
# and $GIT_DIR/info/exclude (this repo only, untracked). A repo's own
# .gitignore/.gitnexusignore can still override either with a `!pattern`
# negation. Skip both entirely with GITNEXUS_NO_GLOBAL_IGNORE=1.
git config --global core.excludesFile ~/.gitignore_global   # applies to every repo
echo "docs/" >> ~/.gitignore_global
echo "build/" >> .git/info/exclude                          # this repo only, untracked

Large files are being skipped

By default the walker skips files larger than 512 KB (see log line Skipped N large files (>512KB)). Raise the threshold via either the CLI flag or the environment variable — both accept a value in KB:

# CLI flag (takes precedence over the env var)
npx gitnexus analyze --max-file-size 2048     # skip only files > 2 MB

# Environment variable (persists across commands)
export GITNEXUS_MAX_FILE_SIZE=2048
npx gitnexus analyze

Values above 32768 KB (32 MB) are clamped to the tree-sitter parser ceiling; invalid values fall back to the 512 KB default with a one-time warning. When an override is active, analyze prints the effective threshold in its startup banner (e.g. GITNEXUS_MAX_FILE_SIZE: effective threshold 2048KB (default 512KB)).

Analyze reports a worker timeout

Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and quarantines a file that repeatedly crashes its worker (respawning the slot so the pool keeps going). If a large repository needs more time per worker job, use either:

# CLI flag, in seconds
npx gitnexus analyze --worker-timeout 60

# Environment variable, in milliseconds
export GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000
npx gitnexus analyze

For repositories with very large source files, GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES controls the worker job byte budget. The default is 8388608 bytes (8 MB).

Worker pool resilience tuning

Four env vars expose the pool's resilience layers (respawn budget, cumulative-timeout cap, circuit breaker, startup handshake). Defaults are tuned for typical repos; bump them when an analyze legitimately needs more retries, or lower them to fail-fast on a known-bad shape.

Variable Default Effect
GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT 3 Max replacement spawns per slot before the slot is dropped from the active rotation.
GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS 5 × subBatchTimeoutMs Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits.
GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD max(3, poolSize) Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool.
GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS 30000 Max wait at pool shutdown for a retired worker still inside native code — terminated at its next JS-safe point instead of mid-native-call, which would abort the process (Napi::Error, #2432).
GITNEXUS_WORKER_READY_TIMEOUT_MS 5000 Startup budget for a parse worker to load its grammar bindings and report {type:'ready'}. Slots that miss it are treated as startup crashes. Raise it on a slow or heavily loaded host where a full pool cold-starting concurrently needs more than 5s.
GITNEXUS_MEMORY off unset (autopilot on)
GITNEXUS_WORKER_HEAP_MB clamp(512, RAM/2/poolSize, 4096) Per-worker V8 old-generation heap cap (#2649). Bounds pool RSS on large repos; a worker exceeding it dies with a real heap error handled by quarantine/respawn.
GITNEXUS_SERVER_ANALYZE_HEAP_MB min(8192, auto cap) Heap for the web/MCP server's forked analyze worker (#2649). Defaults to the historical 8192 MB bounded by the machine/container's RAM-aware auto cap; set an absolute MB value to override.
GITNEXUS_CPP_CAPTURE_BUDGET_MS 20000 Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning (#2432). 0 expires immediately.

Graph cleanup tuning

After scope resolution, analyze prunes inert block-local value symbols (a function-local const/let/var that ends up with only its structural File→DEFINES edge) to keep the graph focused on cross-symbol relationships. Module/file-scope symbols, class members, and any local with a real edge are always kept.

Variable Default Effect
GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS unset Set to 1/true to keep inert block-local value symbols instead of pruning them.

Programmatic callers can pass keepLocalValueSymbols: true in PipelineOptions instead of setting the env var.

Scope-resolution property-key dispatch cap

During scope resolution GitNexus synthesizes CALLS edges through property-key dispatch — call sites like hooks.emitScopeCaptures() where a property key is registered by multiple definitions across the codebase. To keep this fan-in bounded, each property key is capped at 32 registrations: a key registered by more than 32 distinct functions is skipped entirely (no CALLS are synthesized through it), and the dropped key names are surfaced in the analyze log for operator visibility. The cap is calibrated at 2× this repo's own provider table (16 legitimate registrations, one per language provider).

Variable Default Effect
GITNEXUS_MAX_PROPERTY_DISPATCH_FANOUT 32 Per-property-key registration cap in the property-dispatch scope-resolution pass. Set to a positive integer to raise it for repositories whose provider/hook tables exceed the default and lose CALLS coverage on a legitimate key; non-integer or < 1 values fall back to 32. Lowering it tightens the overflow budget.
# A property key registered by 40 functions overflows the default 32 and drops
# all CALLS through it — raise the cap for that repo and rebuild so scope
# resolution reruns.
export GITNEXUS_MAX_PROPERTY_DISPATCH_FANOUT=64
npx gitnexus analyze --force

Scope-resolution dispatch-target cap

During scope resolution GitNexus resolves calls that flow through callable values — function/method references bound to variables, passed as arguments, or stored in maps/tables. To keep that inclusion-based resolution finite, each callable site is capped at 32 dispatch targets. When a site gathers more candidates than the cap it is treated as overflowed and all of its call edges are dropped — a cliff, not a tail, so a repository with a legitimately wide dispatch table (a single callable site resolving to 33+ targets) loses that site's whole call chain. In that case analyze logs callable-value-flow: candidate set exceeded the cap; no partial CALLS emitted alongside a warning carrying the language, the overflowing context, the candidate count, and the cap (32).

Raise the cap for such repositories:

Variable Default Effect
GITNEXUS_MAX_CALLABLE_VALUE_TARGETS 32 Per-callable-site dispatch-target cap in the callable-value-flow scope-resolution pass. Set to a positive integer to raise it for repositories whose wide dispatch tables overflow the default and lose a whole call chain; non-integer or < 1 values fall back to 32. Lowering it tightens the overflow budget.
# A callable site resolving to 48 targets overflows the default 32 and drops
# the chain — raise the cap for that repo and rebuild so scope resolution reruns.
export GITNEXUS_MAX_CALLABLE_VALUE_TARGETS=64
npx gitnexus analyze --force

Hook augmentation and skip diagnostics

The Claude Code / Antigravity hooks keep their stderr silent on normal skip paths so strict hook runners (e.g. Codex PreToolUse) never see unexpected diagnostic output.

When a GitNexus process holds the repo DB write lock (the common case — the MCP server is running, or the DB-lock probe timed out and failed closed), the local CLI augment can't run (LadybugDB is single-writer). Rather than drop the augmentation, the hook hands the agent a short, conditional MCP-query hint on stdout (the sanctioned additionalContext channel) — "if the GitNexus MCP tools are live in this session, call query …" — so an agent that has the tools can still fetch graph-ranked context. The hint is throttled to at most once per repo per window (GITNEXUS_MCP_HINT_THROTTLE_MS, default 10 min; 0 disables), so an owner-locked session isn't nudged on every search. A stale-index reminder, or an already-current index, stays silent.

To see why a hook skipped the CLI augment, set GITNEXUS_DEBUG=1 and re-run the action — the hook writes the reason (e.g. [GitNexus] augment skipped: MCP server owns DB) and the stale-index hint to its stderr:

GITNEXUS_DEBUG=1 <your command>   # surfaces hook skip/diagnostic reasons on stderr

Only GITNEXUS_DEBUG=1 and GITNEXUS_DEBUG=true enable diagnostics; every other value (including 0 and false) is treated as off. Diagnostics go to stderr only — the hook's structured stdout (the JSON the agent consumes) is unaffected.

Privacy

  • All processing happens locally on your machine
  • No code is sent to any server
  • Index stored in .gitnexus/ inside your repo (gitignored)
  • Global registry at ~/.gitnexus/ stores only paths and metadata

Web UI

GitNexus also has a browser-based UI at gitnexus.vercel.app — 100% client-side, your code never leaves the browser.

Local Backend Mode: Run gitnexus serve and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.

License

PolyForm Noncommercial 1.0.0

Free for non-commercial use. Contact for commercial licensing.