* fix(parse): survive non-cloneable worker results so large-repo analyze doesn't crash (#2112) A parse worker delivers its accumulated result to the main thread via postMessage, which structured-clones the payload synchronously on the worker thread and throws a DataCloneError on the first value it can't serialize. The reporter's case was a node `properties` value pointing at a native `toString`. The worker re-posted the throw as {type:'error'}, the pool counted it as a worker death, and under GITNEXUS_WORKER_POOL_SIZE=1 the same graph re-threw on every respawn until the slot's budget was exhausted and the whole parse phase aborted -- defeating even the conservative single-worker workaround. Add a clone-safety net at the worker result boundary. On a clone failure the worker isolates the offending file, strips the non-cloneable value from a plain extraction record (keeping the record -- strictly-missing data, never wrong) or drops a whole ParsedFile so scope-resolution re-derives it on the main thread with intact edge data, records the affected paths on the result, warns naming the field + file so the leak is diagnosable, and re-posts. Healthy runs are byte-identical: the net runs only after a real DataCloneError, so there is zero overhead on the fast path. Skipped paths surface via the parsing processor alongside the skipped-language telemetry. The strip drops the same values the store path's JSON.stringify already silently removes, so store/no-store runs converge. Scope: PR-1 -- failure mode C, the deterministic POOL_SIZE=1 killer. The timeout/native-abort graceful-degradation cascade (failure modes A & B) is coupled to downstream-exclusion + a hard worker watchdog and is tracked as follow-up work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): fail-closed clone-safety recovery + bound recursion depth (#2135 review) The clone-safety recovery path could re-arm the #2112 worker-death cascade it was built to prevent: in postResultCloneSafe the sanitizer call and the re-post sat outside the try/catch, and containsNonCloneable/stripNonCloneable recursed with a cycle guard but no depth bound. A throw inside the sanitizer (a RangeError from a deeply-nested record, reproduced at depth >=3000) escaped to the message handler's {type:'error'}, which under GITNEXUS_WORKER_POOL_SIZE=1 is the respawn-budget-exhaustion abort. Wrap the sanitizer + re-post in their own try/catch so any throw fails closed to a primitive-only {type:'error'} deliberately, and thread a MAX_CLONE_DEPTH bound through both scan/strip functions so an over-deep subtree is treated as non-cloneable (dropped/undefined) instead of overflowing the stack. The isStructuredCloneable catch-all is left broad on purpose — it bounds structuredClone's own internal recursion in the non-plain-object probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): harden clone-safety against throwing getters and detached buffers (#2135 review) Two sanitizer-defeat vectors let the re-post throw a DataCloneError again: - A throwing getter on a record: containsNonCloneable/stripNonCloneable read obj[key], so a getter that throws escaped the scan/strip pass. Read defensively — a throwing property read is treated as non-cloneable (scan returns true, strip drops the property). - A detached ArrayBuffer/TypedArray: both passed buffers/views through unconditionally, but structuredClone rejects a detached one, so the re-post threw. Route buffers/views through the authoritative isStructuredCloneable probe instead. No byteLength heuristic — a legitimately empty new Uint8Array(0) also has byteLength 0 yet clones fine, so a length check would false-positive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): memoize stripped copies so DAG-aliased records aren't over-dropped (#2135 review) stripNonCloneable carried a shared `seen` WeakSet and returned the ORIGINAL (un-stripped) value on revisit. When a non-cloneable was reachable via two paths (a DAG), the second path spliced the original function-bearing object back into the output, so the rebuilt element failed the last-resort isStructuredCloneable guard and the whole record was dropped as "unsalvageable" — contradicting the "record kept, value stripped" contract. Replace the WeakSet with a Map<object, stripped-copy>: allocate the empty copy, memoize it before recursing into children (so cycles return the in-progress copy), and return the memoized copy on revisit. DAG-aliased subtrees now collapse to one shared stripped copy and are kept-and-stripped, not dropped. The array branch moves from .map() to allocate-then-push so its identity can be pre-inserted. Object Map/Set keys aren't identity-preserved across stripping — acceptable because parse-result Maps are primitive-keyed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(parse): single-pass clone-safety scan preserving array identity (#2135 review) makeWorkerResultCloneSafe scanned each dirty array twice — a field-level whole-array containsNonCloneable probe, then a per-element pass — and always reassigned the field. Fold into one per-element pass that builds the output array lazily (copying the clean prefix only once the first dirty element appears) and reassigns the field only when something changed. A fully-clean array is now scanned once and keeps its referential identity; the clean prefix of a dirty array is copied by reference. Behavior is otherwise identical (failure-path-only code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): drop unused generic + pin clone-safe field names to keyof (#2135 review) makeWorkerResultCloneSafe carried a generic `<T extends Record<string,unknown>>` that was never load-bearing (it mutates in place and returns {skipped}), and the call site passed untyped string-literal option sets — so renaming `parsedFiles` or `skippedPaths` would silently disable the drop-whole / skip protection. Drop the generic (plain `Record<string,unknown>` param) and type the option sets at the call site as `Set<keyof ParseWorkerResult>`, so a field rename is now a compile error. The `as unknown as Record<string,unknown>` widening stays — it's the standard cast for a no-index-signature interface (TS rejects a single-step `as`); the function genuinely operates structurally on the result's arrays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): keep the per-file reason in the clone-safety skip log (#2135 review) The processor's skipped-file warning logged only the paths, dropping the per-file reason the worker already attached — losing the distinction between a recoverable "stripped N value(s)" and a whole-record "dropped" entry. Format each entry as `path (reason)` so the aggregate line carries the diagnostic detail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): deterministic findFilePath attribution for ParsedNode (#2135 review) findFilePath swept all child objects one level deep in Object.keys order, so a ParsedNode could be attributed to a sibling child's path-like key instead of its real path at properties.filePath. Check the known `properties` child first, then fall back to the generic sweep, so node attribution is deterministic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): zero skippedPaths in the slim cache result (#2135 review) slimParseWorkerResultsForCache spread the worker result without clearing the clone-safety skippedPaths telemetry, so a sanitized result persisted its skip list into the on-disk parse-cache shard. Replay already ignores the field; zero it (like calls/assignments/parsedFiles) to keep shards lean and the intent explicit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): exercise real postResultCloneSafe wiring + tighten RED control (#2135 review) The integration GREEN worker re-implemented postResultCloneSafe inline, so the production wiring (the {type:'warning'} post + the skippedPaths append) had no coverage, and the RED control asserted a bare .rejects.toThrow() that any failure would satisfy. Extract postResultCloneSafe into a side-effect-free module (post-result.ts) — importing it from the parse-worker entry module would construct the parser, post ready, and attach the real handler — and have the GREEN test worker import and call the real one. Tighten the RED matcher to the actual abort contract (/circuit breaker|consecutive failures|respawn budget|could not be cloned/), which also documents that the raw poison result aborts via the pool's consecutive-failure circuit breaker under POOL_SIZE=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): recover the clone-safety net from any post failure, not only DataCloneError (#2135 review) The V8 structured-clone research surfaced the net's one real correctness hole: structuredClone invokes getters, and a getter that THROWS surfaces its own error (a RangeError, etc.) — NOT a DataCloneError (confirmed against a real MessageChannel). postResultCloneSafe gated recovery on isDataCloneError, so such a throw re-threw past the sanitizer and re-armed, under POOL_SIZE=1, the worker-death cascade the net exists to prevent. Attempt the sanitize + re-post recovery for ANY first-post failure (the sanitizer already reads properties defensively, so a throwing getter is dropped), falling closed to a primitive-only {type:'error'} only if the re-post still fails. Adds an integration case: a node with a throwing getter is recovered and delivered, not re-thrown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): name the exact offending key path in the clone-skip diagnostic (#2135 review) The clone-safety net's skip reason named only the array field + file ("stripped 1 value from nodes"), not the offending property key — which is precisely why the original #2112 leak stayed unpinned. Thread a dotted key path through stripNonCloneable (recording each stripped value's path: properties.toString, meta.data[3], …) and surface the first few in the reason ("from nodes: properties.toString"). Now a single log line — or the contract/strict checks — names the leaking property, so a residual runtime escape can be fixed at source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): clone contract — a representative ParseWorkerResult is structured-cloneable (#2135 review) Shape-regression guard: builds a representative ParseWorkerResult (typed as the real interface) and asserts isStructuredCloneable. Typing it as ParseWorkerResult makes adding a new boundary field a compile error here until the test is updated, and the runtime assert catches a field whose type regresses to a non-cloneable shape — independent of language input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): strict-mode clone gate (GITNEXUS_STRICT_CLONE) — fail loudly instead of silent sanitize (#2135 review) The runtime net's silent recovery in production is exactly what let the original #2112 leak stay unpinned. Add an opt-in strict mode (GITNEXUS_STRICT_CLONE=1, inherited by workers): on a clone failure, postResultCloneSafe THROWS with the exact offending key path instead of sanitizing + delivering, so a leak introduced by a future provider/extractor change fails loudly at its origin (CI/dev) rather than being quietly stripped. Off in production, where the net keeps the run alive. Adds a self-contained integration case (sets the flag, asserts the poison run rejects with the key path) and skips the synthetic-poison suite under a global strict run (its value there is running the REAL-extractor integration tests under strict). Wiring a strict CI lane (GITNEXUS_STRICT_CLONE=1 on a vitest integration step) is left to the maintainer — it needs a green full-suite verification and touches the protected workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): don't ship pipelineResult across the analyze-worker IPC boundary (#2112) The forked analyze worker reports completion to the parent over child_process IPC, which uses Node's DEFAULT 'json' serialization (api.ts forks with no `serialization:` option). `AnalyzeResult.pipelineResult` is populated on every successful analysis and carries `pipelineResult.graph` — the live KnowledgeGraph closure object. Sending the raw result is wrong three ways: (1) the graph's nodes/relationships getters force-materialize the entire graph into two arrays and JSON-stringify them on every analyze, discarded immediately (a multi-hundred-MB no-op on a large repo — the #2112 scenario); (2) the graph's methods are own function properties that JSON drops silently, so a surviving graph is a husk whose forEachNode() throws far from the cause; (3) a BigInt/circular value anywhere in the payload makes process.send throw TypeError synchronously — caught and re-sent as {type:'error'}, mis-reporting a SUCCESSFUL analysis (DB already written) as a FAILURE. This is the #2112 failure family on the server path, and unlike the parse-worker result boundary it has no clone-safety net. The parent (api.ts) reads only result.repoName; pipelineResult's real consumers (CLI skill generation, cli/analyze.ts) call runFullAnalysis in-process and never cross this fork. So project the result down to an explicit JSON-safe allowlist of scalar fields. Typed as Omit<AnalyzeResult,'pipelineResult'> so a future non-serializable field added to AnalyzeResult fails to compile until handled here deliberately. Found by the #2112 cross-process serialization-boundary audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): Cloneable<T> + assertCloneable() compile-time clone-boundary guard (#2143) The runtime clone-safety net is the production backstop; this is its compile-time complement. The worker result is plain data except a few `unknown`-typed sinks (a node's `properties` bag, the provider `extractTemplateConstraints` / `collectCaptureSideChannel` hook returns) — `unknown` lets a non-serializable value (a function, a leaked tree-sitter SyntaxNode, …) cross the structured-clone boundary with no compile-time guard. That is the structural hole #2112 leaked through. `Cloneable<T>` is a homomorphic recursive mapped type that maps a function or symbol member to `never`, so a struct carrying one is no longer assignable to its own `Cloneable<T>`. `assertCloneable(value)` is a runtime identity (zero cost) whose parameter is `T extends Cloneable<T> ? T : Cloneable<T>`, so a clone-unsafe argument fails to compile, naming the offending key. Because it is a homomorphic mapped type it preserves `interface` shapes and `readonly` modifiers and needs NO index signature on the payload types — this sidesteps the "closed interface is not assignable to a recursive index-signature type" wall that blocked the original value-typed-`Cloneable` attempt (the reason #2143 was deferred from PR #2135). The conditional parameter type avoids the `T extends Cloneable<T>` circular-constraint error. Tests: runtime identity contract, plus type-level @ts-expect-error assertions (enforced by tsconfig.test.json) that a function/symbol member is rejected and clean interface payloads are accepted. Applied to the real provider hooks in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): guard provider clone-boundary hooks with assertCloneable (#2143) Apply the compile-time guard to the provider hooks that feed the `unknown`-typed worker-result sinks, so a future non-serializable value in their payloads is a compile error at the source site rather than a runtime DataCloneError at the worker post: - C++ extractTemplateConstraints (CppConstraintPayload) - C++ collectCaptureSideChannel (CppCaptureSideChannel) - C collectCaptureSideChannel (CCaptureSideChannel) - Kotlin collectCaptureSideChannel (KotlinCaptureSideChannel) The C++ template-constraint adapter previously returned `unknown`; it now returns the concrete `CppConstraintPayload | undefined` and routes its payload through `assertCloneable`. The side-channel hooks are wrapped at their provider wiring sites. `assertCloneable` is a runtime identity, so behavior is unchanged (C static-linkage + C++ constraint suites stay green); the guarantee is the type-check — src tsc now proves every nested member of those real payload trees is structured-clone safe. Test: type-level assertions (enforced by tsconfig.test.json) that each concrete payload type is `Cloneable<T>`, INDEPENDENT of the provider wiring — so the regression is caught even if the assertCloneable wrapper is later removed. Proven non-vacuous (a function-bearing type fails the same assertion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): scan an array's non-index own properties in the clone sanitizer (#2135 review) structuredClone serializes an array's NON-index own-enumerable properties (e.g. `arr.meta = fn`) and throws DataCloneError on a non-cloneable one. The clone sanitizer's array branches iterated numeric indices only, so such an array was waved through (containsNonCloneable returned false, makeWorkerResultCloneSafe left the field unrewritten with skipped:[]) — the re-post then threw, fell through to the fail-closed {type:'error'}, and re-armed the POOL_SIZE=1 cascade the net exists to prevent. Add isArrayIndexKey() and, in BOTH containsNonCloneable and stripNonCloneable array branches (kept in lockstep), scan/strip the non-index own-enumerable keys after the index loop. A cloneable non-index prop is carried onto the stripped copy; a non-cloneable one is stripped and recorded. Not reachable from current parse output (no extractor attaches non-index array props) — a defense-in-depth hole closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): contain a throw inside the clone sanitizer instead of escaping to fail-closed (#2135 review) findFilePath was documented "never throws" but read element properties unguarded in its generic sweep — a throwing getter at a non-path key (or a Proxy with a throwing ownKeys trap) threw out of makeWorkerResultCloneSafe, past postResultCloneSafe's recovery, to the fail-closed {type:'error'} that under POOL_SIZE=1 re-arms the cascade the net prevents. Likewise a Proxy with a throwing getPrototypeOf trap throws inside containsNonCloneable's instanceof checks. - findFilePath/pathFromChild now read via safeGet (try/catch) and guard Object.keys, honoring the "never throws" contract. - Each element's sanitize in makeWorkerResultCloneSafe is wrapped: a throw during scan/strip drops that one element (recorded as "sanitizer error") rather than sinking the whole result — so one pathological element can't fail-close the run. - Corrected the makeWorkerResultCloneSafe JSDoc ("ONLY after a DataCloneError" → after ANY post failure, matching the caller) and documented the deliberate failure-path double-traversal (the non-allocating pre-scan is what preserves clean-element referential identity). Tests: a throwing getter on a path-less element is stripped & delivered (not escaped); a Proxy structural-trap element is dropped, clean siblings survive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): add a final cloneable postcondition gate to the clone sanitizer (#2135 review) makeWorkerResultCloneSafe rewrote only ARRAY result fields, so a future non-array sink (a nested object / Map result field) carrying a non-cloneable value — or an array field whose own non-index property the element loop didn't reach — would survive the sanitizer and throw on the re-post. Add a final `if (!isStructuredCloneable(result))` gate that strips any remaining offending field in place, making "the returned result is structured-cloneable" a hard postcondition independent of future ParseWorkerResult shape. Failure-path-only and a no-op once the array loop already made the result clean (the per-field probe short-circuits every clean field, so it adds no work or skip entries then). Tests: a function on a non-array result field is stripped & the result becomes cloneable; the gate adds no skip entry when the array loop already cleaned up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): reject an `any`-typed member in the Cloneable<T> compile-time guard (#2135 review) `Cloneable<any>` previously resolved to `any` (not `never`), so a payload with an `any`-typed member — the most likely escape hatch, since `unknown` is already blocked — passed `assertCloneable` with no compile error. Add an `IsAny<T>` branch (the canonical `0 extends 1 & T` probe) as the FIRST arm so `any` resolves to `never`, matching how `unknown` is already rejected. It must precede the primitive arm: `any extends CloneablePrimitive` would otherwise resolve to `any` and re-admit it. The IsAny-first arm perturbs inference for a bare `undefined` literal argument (T infers as `unknown` → never); real consumers pass `X | undefined` unions (the provider hooks), which are unaffected (src tsc clean), so the runtime identity test now uses a `string | undefined` value — the realistic shape. Tests: an `any` member fails `assertCloneable` (@ts-expect-error, enforced by tsconfig.test.json) and `Cloneable<any>` resolves to `never` at the type level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): type the analyze-worker IPC projection as a Pick allowlist, not Omit (#2135 review) `AnalyzeResultIpc = Omit<AnalyzeResult,'pipelineResult'>` kept every other field in the type — including optional ones like `isPrimaryBranch?` — so the type advertised a field the runtime allowlist never sends, and the doc-comment's "a future field fails to compile until handled here" only held for REQUIRED fields. Switch to `Pick<AnalyzeResult, …the six scalar fields…>`: the allowlist IS the type, so the projection return literal is exhaustive by construction (omitting a key is a compile error) and a new `AnalyzeResult` field is simply absent from the wire until deliberately added here. `isPrimaryBranch` is intentionally excluded (nothing consumes it server-side over this fork; the parent reads only `repoName`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): remove the now-dead isDataCloneError export (#2135 review) postResultCloneSafe recovers on ANY fast-path post failure and never inspects the error type (a throwing getter surfaces a RangeError, not a DataCloneError — gating on the type was the original net-gap bug). isDataCloneError has no production caller; it was only exercised by its own unit test. Remove the function and that test block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): use the exported SkippedPath type in parsing-processor (#2135 review) The clone-safety telemetry accumulator inlined `Array<{path,reason}>` — a structural duplicate of the exported `SkippedPath`. Import and use the canonical type so a future rename of its fields is a compile error here instead of a silent structural drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(parse): document the cloneable-return contract on the worker-boundary hooks (#2135 review) extractTemplateConstraints and collectCaptureSideChannel return `unknown` and feed values across the worker structured-clone boundary, but the hook contracts didn't state the cloneability requirement — a future language implementing them without care could leak a non-serializable value. Document that the return MUST be structured-clone-safe and should be wrapped with assertCloneable, so the guarantee is a compile error at the source (#2143). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): assert the clone-skip telemetry surfaces in the GREEN integration case (#2135 review) The GREEN clone-safety integration test asserted only graph content (all files present), not that the skippedPaths / {type:'warning'} wiring its docstring claims to cover actually fired. Capture the production logger via _captureLogger and assert the sanitize telemetry names the offending file (poison.ts) AND the exact stripped key path (properties.toString) — proving the worker's skippedPaths append + the parsing-processor warning surfaced end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(server): cover the IPC projection against a real KnowledgeGraph (#2135 review) The IPC projection tests used a hand-built hostile object. Add a case that puts a real createKnowledgeGraph (whose nodes/relationships getters would materialize the whole graph under JSON.stringify) in pipelineResult and asserts the projection drops it entirely — the serialized payload stays under 300 bytes (a materialized 50-node graph would be thousands), with the scalar fields intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): cover the unsalvageable-drop branch and the skippedPaths merge union (#2135 review) Two untested clone-safety branches from the tri-review: - "dropped unsalvageable": a dirty element whose stripped copy is STILL not structured-cloneable must be dropped, not delivered (else the re-post throws). Add a deterministic test (a non-plain member with a stateful getter that the strip-time probe sees clean but that turns into a function on the post-strip verification) asserting the element is dropped and the run survives. - mergeResult skippedPaths union across sub-batches. mergeResult (and its appendAll helper) was module-private in the parse-worker ENTRY module, which a main-thread test can't import (it runs MessagePort setup). Extract it to a side-effect-free result-merge.ts (mirroring post-result.ts) and unit-test the union (including the `??=` target-init path), the skippedLanguages sum, and array append. parse-worker imports it back; verified the built worker still parses + merges via the real-worker integration path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(parse): root-prettier format the clone-safety review-fix files (#2135 review) Clears the failing `quality / format` CI gate (root prettier, not the gitnexus-local config). Reformats the pre-existing #2143 wrapping lines in c-cpp.ts + kotlin.ts plus the clone-safety review-fix files touched in this PR-update (clone-safety.ts and the new/updated tests). Formatting-only — no behavior change; tsc, the type-level assertions (tsconfig.test.json), and the unit + integration suites stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): avoid js/trivial-conditional in the type-level clone assertions (#2135 review) CodeQL flagged the `expect(a && b && c).toBe(true)` lines in the type-level test assertions as js/trivial-conditional: after type erasure the operands are constant `true`, so the `&&` chain always evaluates the same. Replace the `&&` chain with array equality (`expect([...]).toEqual([true, ...])`) — no conditional, and the real assertions remain the `const x: …IsNever = true` / `: IsCloneable<…> = true` annotations (enforced by tsconfig.test.json, which fail to compile if a guard regresses). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .claude-plugin | ||
| .cursor | ||
| .devcontainer | ||
| .gemini/commands | ||
| .github | ||
| .history/gitnexus | ||
| .husky | ||
| .sisyphus/drafts | ||
| deploy/kubernetes | ||
| eslint-rules | ||
| eval | ||
| gitnexus | ||
| gitnexus-claude-plugin | ||
| gitnexus-cursor-integration | ||
| gitnexus-shared | ||
| gitnexus-test-setup | ||
| gitnexus-web | ||
| pr-swarm-review | ||
| .cursorrules | ||
| .dockerignore | ||
| .env.example | ||
| .git-blame-ignore-revs | ||
| .gitattributes | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .mcp.json | ||
| .prettierignore | ||
| .prettierrc | ||
| .windsurfrules | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| compound-engineering.local.md | ||
| CONTRIBUTING.md | ||
| docker-compose.yaml | ||
| docker-server.mjs | ||
| docker-server.test.mjs | ||
| Dockerfile.cli | ||
| Dockerfile.web | ||
| DoD.md | ||
| eslint.config.mjs | ||
| GUARDRAILS.md | ||
| LICENSE | ||
| llms.txt | ||
| MIGRATION.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| RUNBOOK.md | ||
| SECURITY.md | ||
| skills.mdm | ||
| swift-ingestion-gaps.md | ||
| TESTING.md | ||
| type-resolution-roadmap.md | ||
| type-resolution-system.md | ||
GitNexus
⚠️ Important Notice: GitNexus has NO official cryptocurrency, token, or coin. Any token/coin using the GitNexus name on Pump.fun or any other platform is not affiliated with, endorsed by, or created by this project or its maintainers. Do not purchase any cryptocurrency claiming association with GitNexus.
Join the official Discord to discuss ideas, issues etc!
Enterprise (SaaS & Self-hosted) - akonlabs.com
Building nervous system for agent context.
Indexes any codebase into a knowledge graph — every dependency, call chain, cluster, and execution flow — then exposes it through smart tools so AI agents never miss code.
https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
Like DeepWiki, but deeper. DeepWiki helps you understand code. GitNexus lets you analyze it — because a knowledge graph tracks every relationship, not just descriptions.
TL;DR: The Web UI is a quick way to chat with any repo. The CLI + MCP is how you make your AI agent actually reliable — it gives Cursor, Claude Code, Antigravity, Codex, and friends a deep architectural view of your codebase so they stop missing dependencies, breaking call chains, and shipping blind edits. Even smaller models get full architectural clarity, making it compete with Goliath models.
Star History
Two Ways to Use GitNexus
| CLI + MCP | Web UI | |
|---|---|---|
| What | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser |
| For | Daily development with Cursor, Claude Code, Antigravity, Codex, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
| Scale | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode |
| Install | npm install -g gitnexus |
No install — gitnexus.vercel.app |
| Storage | LadybugDB native (fast, persistent) | LadybugDB WASM (in-memory, per session) |
| Parsing | Tree-sitter native bindings | Tree-sitter WASM |
| Privacy | Everything local, no network | Everything in-browser, no server |
Bridge mode:
gitnexus serveconnects the two — the web UI auto-detects the local server and can browse all your CLI-indexed repos without re-uploading or re-indexing.
Enterprise
GitNexus is available as an enterprise offering - either as a fully managed SaaS or a self-hosted deployment. Also available for commercial use of the OSS version with proper licensing.
Enterprise includes:
- PR Review - automated blast radius analysis on pull requests
- Auto-updating Code Wiki - always up-to-date documentation (Code Wiki is also available in OSS)
- Auto-reindexing - knowledge graph stays fresh automatically
- Multi-repo support - unified graph across repositories
- OCaml support - additional language coverage
- Priority feature/language support - request new languages or features
Upcoming:
- Auto regression forensics
- End-to-end test generation
👉 Learn more at akonlabs.com
💬 For commercial licensing or enterprise inquiries, ping us on Discord or drop an email at founders@akonlabs.com
Development
- ARCHITECTURE.md — packages, index → graph → MCP flow, where to change code
- RUNBOOK.md — analyze, embeddings, stale index, MCP recovery, CI snippets
- GUARDRAILS.md — safety rules and operational “Signs” for contributors and agents
- CONTRIBUTING.md — license, setup, commits, and pull requests
- TESTING.md — test commands for
gitnexusandgitnexus-web
CLI + MCP (recommended)
The CLI indexes your repository and runs an MCP server that gives AI agents deep 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?
npxcan crash during install withCannot destructure property 'package' of 'node.target'(an npm/arborist bug, before GitNexus runs). Use pnpm instead — it builds the native deps explicitly:pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyzeOr install globally (
npm install -g gitnexus@latest) and rungitnexus analyze. See #1939.
To configure MCP for your editor, run npx gitnexus setup once — or set it up manually below.
Faster install (no C++ toolchain needed): set
GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1beforenpm install -g gitnexusto skip the vendored grammar materialize/build fortree-sitter-dart,tree-sitter-proto,tree-sitter-swift, andtree-sitter-kotlin— those four won't be parsed, but install completes in seconds withoutpython3/make/g++. Strict=1only — any other value falls through to the rebuild. See thetree-sitter-kotlinnote below.About
tree-sitter-kotlin: like Dart/Proto/Swift, Kotlin is a vendored grammar (undergitnexus/vendor/tree-sitter-kotlin). Upstreamtree-sitter-kotlinships source only (no prebuilt binaries), so GitNexus builds the Kotlin platform prebuilds itself (via thebuild-tree-sitter-prebuildsGitHub Actions workflow) and vendors them — the same uniform pipeline now used for Dart, Proto, and Swift (Swift's prebuilds were originally copied from upstream; they're now GitNexus-cross-built too).node-gyp-buildselects the right.nodeat require time, so no C/C++ toolchain is needed. If no prebuild matches your platform-arch, only Kotlin (.kt/.kts) parsing is unavailable; the rest ofgitnexusis unaffected.
MCP Setup
gitnexus setup auto-detects your editors and writes the correct global MCP config. You only need to run it once.
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 | — | MCP + Skills |
| Windsurf | Yes | — | — | MCP |
| OpenCode | Yes | Yes | — | MCP + Skills |
Claude Code gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
¹ Antigravity hooks follow the Gemini CLI hooks reference (Antigravity 2.0 is the documented successor to Gemini CLI). Augmentation runs in
AfterToolbecauseBeforeToolhas no context-injection channel in the Gemini contract — the agent sees graph context appended to the tool result viahookSpecificOutput.additionalContext. Stale-index hints land in the same channel after a successfulgit commit/merge/rebase/cherry-pick/pull. The schema may evolve if Antigravity-specific hook docs diverge from Gemini CLI's; the implementation will track those changes.
Community Integrations
Built by the community — not officially maintained, but worth checking out.
| Project | Author | Description |
|---|---|---|
| pi-gitnexus | @tintinweb | GitNexus plugin for pi — pi install npm:pi-gitnexus |
| gitnexus-stable-ops | @ShunsukeHayashi | Stable ops & deployment workflows (Miyabi ecosystem) |
Have a project built on GitNexus? Open a PR to add it here!
If you prefer manual configuration:
Recommended for fastest startup: install gitnexus globally (
npm i -g gitnexus) and rungitnexus setup— this writes an absolute-path MCP config that bypassesnpxentirely. The pinned-npxsnippets below are a quickstart fallback; on a cold cache thenpxinstall can exceed Claude Code'sMCP_TIMEOUTdefault (~30s).
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):
codex mcp add gitnexus -- npx -y gitnexus@latest mcp
Cursor (~/.cursor/mcp.json — global, works for all projects):
{
"mcpServers": {
"gitnexus": {
"command": "npx",
"args": ["-y", "gitnexus@latest", "mcp"]
}
}
}
Antigravity (Google) — ~/.gemini/antigravity/mcp_config.json:
{
"mcpServers": {
"gitnexus": {
"command": "npx",
"args": ["-y", "gitnexus@latest", "mcp"]
}
}
}
gitnexus setupalso merges anAfterToolentry into~/.gemini/settings.json(under the canonical Gemini CLI hooks schema) and installs skills to~/.gemini/antigravity/skills/. Existing user hooks are preserved. The hook adapter's path is rewritten at install time, so rungitnexus setuprather than hand-editing.
OpenCode (~/.config/opencode/config.json):
{
"mcp": {
"gitnexus": {
"type": "local",
"command": ["gitnexus", "mcp"]
}
}
}
Codex (~/.codex/config.toml for system scope, or .codex/config.toml for project scope):
[mcp_servers.gitnexus]
command = "npx"
args = ["-y", "gitnexus@latest", "mcp"]
CLI Commands
gitnexus setup # Configure MCP for your editors (one-time)
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 --skills # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-skills # Skip installing .claude/skills/gitnexus/ skill files
gitnexus analyze --default-branch develop # Branch used in the generated regression-compare example (base_ref)
gitnexus analyze --skip-git # Index folders that are not Git repositories
gitnexus analyze --embeddings [limit] # Enable embedding generation (slower, better search)
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
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 analyze --workers <n> # Parse worker pool size (>=1; default: cores-1, capped at 16, auto-sized to the repo). 0 is rejected — there is no sequential mode.
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
gitnexus serve # Start local HTTP server (multi-repo) for web UI connection
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 repository wiki from knowledge graph
gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
gitnexus wiki --base-url <url> # Wiki with custom LLM API base URL
gitnexus publish # Notify the understand-quickly registry (opt-in, see below)
# 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 uninstallreversesgitnexus 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--forceto apply. Per-repo indexes (gitnexus clean --all) and the global npm package (npm uninstall -g gitnexus) are left for you to remove.
If analyze reports a worker parse timeout on a large or unusual repository, it keeps running and falls back safely. To give slow worker jobs more time, use gitnexus analyze --worker-timeout 60 or set GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000. For very large files, GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES controls the worker job byte budget.
Embeddings node limit
gitnexus analyze --embeddings generates semantic search vectors with a default 50,000-node safety cap to protect memory on large repositories. Override the cap when you know the host has enough memory for a larger graph, or disable it entirely for a one-off full embeddings run.
# Generate embeddings with the default 50,000 node safety cap
gitnexus analyze --embeddings
# Disable the safety cap entirely
gitnexus analyze --embeddings 0
# Use a custom cap
gitnexus analyze --embeddings 100000
If embeddings are skipped on a large repository, the indexed graph likely exceeds the default safety cap. Re-run with gitnexus analyze --embeddings 0 to remove the cap, or gitnexus analyze --embeddings <n> to choose a higher limit while still keeping memory bounded.
Project config (.gitnexusrc)
Commit a .gitnexusrc JSON file at the repo root to preconfigure recurring analyze options per project, instead of re-passing the same flags every run. It is read from the resolved repo root (not .gitnexus/, which is gitignored index storage). CLI flags always override .gitnexusrc.
{
// Default branch used in the generated regression-compare example (base_ref).
// Use this so a project on `develop`/`master` doesn't get "main" rewritten
// over its fix on every analyze. (Alias: "branch".)
"defaultBranch": "develop",
"skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md
"skipSkills": true, // don't install .claude/skills/gitnexus/
"embeddings": true, // generate embeddings by default
"workerTimeout": 60
}
A nested analyze block is also accepted (and overrides flat keys for the same option):
{ "analyze": { "defaultBranch": "develop", "skipSkills": true } }
Notes:
- The default branch is resolved as:
--default-branch>.gitnexusrcdefaultBranch/branch> auto-detectedorigin/HEAD>main. skipContextFiles/skipAiContextare aliases forskipAgentsMd— they skip theAGENTS.md/CLAUDE.mdblock only. They do not implyskipSkills.indexOnlyis the stronger option that skips all file injection.- Supported keys:
defaultBranch(branch),skipAgentsMd(skipContextFiles,skipAiContext),skipSkills,indexOnly,stats/noStats,embeddings,dropEmbeddings,name,allowDuplicateName,maxFileSize,workerTimeout,walCheckpointThreshold,workers,embeddingThreads,embeddingBatchSize,embeddingSubBatchSize,embeddingDevice. - The file is JSON only. Unknown keys and invalid values fail fast with an actionable error before analysis starts.
Environment variables
Most analyze knobs are also CLI flags (--workers, --worker-timeout, --max-file-size, --verbose). Use the env-var form when you'd otherwise repeat the same flag every run, or when invoking GitNexus from a long-running host (MCP server, eval-server, CI shell) that already manages its own environment. CLI flags take precedence over env vars; env vars take precedence over built-in defaults.
| Variable | Default | Effect | Tune when… |
|---|---|---|---|
GITNEXUS_WORKER_POOL_SIZE |
cores - 1, capped at 16 |
Parse worker pool size (must be ≥ 1). Equivalent to --workers <n>. The worker pool is the sole parse path — there is no sequential parser, so 0 is rejected with an actionable error (the pool self-heals via quarantine + respawn). |
Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set 1 for a single-worker pool — not 0. |
GITNEXUS_PARSE_CHUNK_CONCURRENCY |
2 |
Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. | Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock. |
GITNEXUS_VERBOSE |
unset | When 1, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to --verbose. |
Debugging an analyze that "completed" but seems to have missed files; tuning --workers / chunk concurrency against observable throughput. |
GITNEXUS_PROFILE_DEFERRED |
unset | When 1, emits [deferred-profile] timing/progress logs for the post-chunk deferred resolution band (imports → heritage → buildHeritageMap → legacy call resolution). Implied by GITNEXUS_VERBOSE. |
Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise. |
GITNEXUS_PROFILE_DEFERRED_SLOW_MS |
3000 (verbose) / 5000 |
Per-file threshold in ms above which processCallsFromExtracted emits a slow file … log line. Parsed via Number(): accepts integers (5000), scientific notation (2.5e3), decimals (.5), and hex (0x10). Non-finite or non-positive values fall back to the default. |
Hunting a few outlier files dominating the deferred call-resolution stage; lower to surface more, raise to focus only on the worst. |
GITNEXUS_MAX_FILE_SIZE |
512 (KB) |
Walker skip threshold in KB. Hard cap is 32768 (tree-sitter buffer ceiling). Equivalent to --max-file-size <kb>. |
Indexing repos with intentionally-large source files (generated parsers, vendored bundles) that should still be parsed. |
GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS |
30000 |
Worker idle timeout in milliseconds before retry/fallback. Equivalent to --worker-timeout <seconds> × 1000. |
Slow-parsing files (large minified JS, deeply-nested TS types) that legitimately need more than 30s. |
GITNEXUS_WAL_CHECKPOINT_THRESHOLD |
67108864 (64 MiB) |
LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to --wal-checkpoint-threshold <bytes>. -1 keeps LadybugDB's stock threshold (~16 MiB). Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
You need a larger or smaller WAL auto-checkpoint threshold for your analyze workload. |
GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES |
8388608 (8 MB) |
Per-job byte budget the pool will send to a worker in one postMessage. |
Very large individual files; mostly diagnostic — bumping past 8 MB risks structured-clone memory pressure. |
GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT |
3 |
Max replacement spawns per worker slot before the slot is dropped from the active rotation. Bounds respawn loops on a chronically-crashing slot. | Hosts where a flaky worker should retry more (raise) or fail-fast (lower) before the slot is dropped. |
GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS |
5 × subBatchTimeoutMs |
Total retry wall-time budget per job before quarantining. Combined with timeoutBackoffFactor, prevents exponentially-growing retries from stalling for hours. |
Slow files that legitimately need long total retry windows; lower to fail-fast on stalls. |
GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD |
max(3, poolSize) |
Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, every subsequent dispatch rejects until a fresh pool is created. | Hosts where a SIGSEGV-prone native grammar should trip the breaker sooner; CI runners that should fail loudly. |
GITNEXUS_CHUNK_BYTE_BUDGET |
2097152 (2 MB) |
Chunk boundary used for cache-key composition and dispatch. Smaller = finer-grained cache hits but more dispatch overhead. | Tuning incremental-analyze cache behavior on monorepos. |
GITNEXUS_NO_GITIGNORE |
unset | When set, skips .gitignore parsing. .gitnexusignore is still honored. |
Indexing a repo whose .gitignore excludes files you actually want indexed (e.g., generated code committed for cross-repo lookup). |
GITNEXUS_SKIP_OPTIONAL_GRAMMARS |
unset | When =1 strictly, skips the vendored grammar materialize for tree-sitter-dart, tree-sitter-proto, tree-sitter-swift, and tree-sitter-kotlin at install time (and the Dart/Proto source builds). Those four won't be parsed; the install still succeeds. |
Installing on a host without a C++ toolchain or where the vendored prebuilds don't match; willing to skip Dart/Proto/Swift/Kotlin parsing. |
Publishing to understand-quickly (opt-in)
looptech-ai/understand-quickly is a public registry of code-knowledge graphs that lists gitnexus@1 as a first-class format. After registering your repo once (npx @understand-quickly/cli add or the wizard), gitnexus publish fires a single repository_dispatch event so the registry resyncs your entry on demand instead of waiting for the nightly job.
It is opt-in and a no-op without UNDERSTAND_QUICKLY_TOKEN — a fine-grained GitHub PAT with Repository dispatches: write on the registry repo. Nothing else happens; no graph file is uploaded. See the protocol spec for the full contract.
What Your AI Agent Gets
16 tools exposed via MCP (11 per-repo + 5 group):
| Tool | What It Does | repo Param |
|---|---|---|
list_repos |
Discover all indexed repositories (paginated — limit/offset) |
— |
query |
Process-grouped hybrid search (BM25 + semantic + RRF) | Optional |
context |
360-degree symbol view — categorized refs, process participation | Optional |
impact |
Blast radius analysis with depth grouping and confidence | Optional |
detect_changes |
Git-diff impact — maps changed lines to affected processes | Optional |
rename |
Multi-file coordinated rename with graph + text search | Optional |
cypher |
Raw Cypher graph queries | Optional |
group_list |
List configured repository groups | — |
group_sync |
Extract contracts and match across repos/services | — |
group_contracts |
Inspect extracted contracts and cross-links | — |
group_query |
Search execution flows across all repos in a group | — |
group_status |
Check staleness of repos in a group | — |
When only one repo is indexed, the
repoparameter is optional. With multiple repos, specify which one:query({query: "auth", repo: "my-app"}).
Resources for instant context:
| Resource | Purpose |
|---|---|
gitnexus://repos |
List all indexed repositories (read this first) |
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 |
2 MCP prompts for guided workflows:
| 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 |
4 agent skills installed to .claude/skills/ automatically:
- 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
Repo-specific skills generated with --skills:
When you run gitnexus analyze --skills, GitNexus detects the functional areas of your codebase (via Leiden community detection) and generates a SKILL.md file for each one under .claude/skills/generated/. Each skill describes a module's key files, entry points, execution flows, and cross-area connections — so your AI agent gets targeted context for the exact area of code you're working in. Skills are regenerated on each --skills run to stay current with the codebase.
Multi-Repo MCP Architecture
GitNexus uses a global registry so one MCP server can serve multiple indexed repos. No per-project MCP config needed — set it up once and it works everywhere.
flowchart TD
subgraph CLI [CLI Commands]
Setup["gitnexus setup"]
Analyze["gitnexus analyze"]
Clean["gitnexus clean"]
List["gitnexus list"]
end
subgraph Registry ["~/.gitnexus/"]
RegFile["registry.json"]
end
subgraph Repos [Project Repos]
RepoA[".gitnexus/ in repo A"]
RepoB[".gitnexus/ in repo B"]
end
subgraph MCP [MCP Server]
Server["server.ts"]
Backend["LocalBackend"]
Pool["Connection Pool"]
ConnA["LadybugDB conn A"]
ConnB["LadybugDB conn B"]
end
Setup -->|"writes global MCP config"| CursorConfig["~/.cursor/mcp.json"]
Analyze -->|"registers repo"| RegFile
Analyze -->|"stores index"| RepoA
Clean -->|"unregisters repo"| RegFile
List -->|"reads"| RegFile
Server -->|"reads registry"| RegFile
Server --> Backend
Backend --> Pool
Pool -->|"lazy open"| ConnA
Pool -->|"lazy open"| ConnB
ConnA -->|"queries"| RepoA
ConnB -->|"queries"| RepoB
How it works: Each gitnexus analyze stores the index in .gitnexus/ inside the repo (portable, gitignored) and registers a pointer in ~/.gitnexus/registry.json. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the repo parameter is optional on all tools — agents don't need to change anything.
Web UI (browser-based)
A client-side graph explorer and AI chat — your code never leaves your machine.
Try it now: gitnexus.vercel.app — run npx gitnexus@latest serve locally and the page auto-connects to your local backend.
Or run the frontend locally:
git clone https://github.com/abhigyanpatwari/gitnexus.git
cd gitnexus/gitnexus-shared && npm install && npm run build
cd ../gitnexus-web && npm install
npm run dev
# Then in another terminal, start the backend the frontend connects to:
npx gitnexus@latest serve
Docker
The official Docker setup ships two signed images orchestrated by docker-compose.yaml. Each image is published to both GitHub Container Registry (GHCR) and Docker Hub — same build, same digest, same Cosign signature — so pick whichever registry you prefer:
| Purpose | GHCR (default in docker-compose.yaml) |
Docker Hub mirror |
|---|---|---|
CLI / gitnexus serve backend (HTTP API on port 4747, MCP, indexer) |
ghcr.io/abhigyanpatwari/gitnexus:latest |
akonlabs/gitnexus:latest |
Static web UI (port 4173) |
ghcr.io/abhigyanpatwari/gitnexus-web:latest |
akonlabs/gitnexus-web:latest |
Heads-up — image rename. Earlier releases published the web UI under
ghcr.io/abhigyanpatwari/gitnexus. Starting with the introduction of the bundled backend, that slug now hosts the CLI/server image and the UI moved toghcr.io/abhigyanpatwari/gitnexus-web. The previous tags remain available for pulling, but new versions are only published under the new slugs. Update yourdocker run/ compose files accordingly (or just adopt the bundled compose).
One-command setup
docker compose up -d
This starts the server on http://localhost:4747 and the web UI on
http://localhost:4173. The UI auto-detects the server because the browser
runs on the host and reaches the container via the mapped port.
A named volume (gitnexus-data) persists the global registry, indexes, and
cloned repos at /data/gitnexus inside the server container. To make repos on
your host machine indexable, set WORKSPACE_DIR before bringing the stack up:
WORKSPACE_DIR=$HOME/code docker compose up -d
# Inside the server container the directory is mounted read-only at /workspace.
docker compose exec gitnexus-server gitnexus index /workspace/my-repo
Direct docker run
# Server
docker run --rm -d \
--name gitnexus-server \
-p 4747:4747 \
-v gitnexus-data:/data/gitnexus \
ghcr.io/abhigyanpatwari/gitnexus:latest
# Web UI
docker run --rm -d \
--name gitnexus-web \
-p 4173:4173 \
ghcr.io/abhigyanpatwari/gitnexus-web:latest
Optional env file (override image tags, container names, ports, workspace dir):
cp .env.example .env
docker compose --env-file .env up -d
Versioning & supply-chain protection
The Docker images are version-locked to the npm package:
- Stable images are only published from
vX.Y.Zgit tags (viadocker.ymltriggered directly by the tag push), and the workflow refuses to build unless the tag exactly matchesgitnexus/package.json's version. Soghcr.io/abhigyanpatwari/gitnexus:1.6.2(and its Docker Hub mirrorakonlabs/gitnexus:1.6.2) is byte-for-byte the same release asnpm install gitnexus@1.6.2— no drift, no floating builds frommain. Both registries receive the same digest from a single build step, so you can pull from either and the signature verifies identically. - Release-candidate images (e.g.
:1.7.0-rc.1) are published alongside each RC npm release. They are built bypublish.ymlcallingdocker.ymlas a reusable workflow after the RC tag is created and pushed. :latestis auto-promoted only from non-prerelease tags by the Docker metadata action, so it always points at a real, npm-published version.
Both images are signed with Cosign keyless signing using the
workflow's GitHub OIDC identity, and shipped with build provenance and SBOM
attestations. This is your protection against supply-chain attacks: even if
an attacker republishes a same-named image elsewhere (or somehow pushes to a
typo-squatted registry), they cannot forge a Cosign signature tied to
abhigyanpatwari/GitNexus's docker.yml. Always verify before pulling into
sensitive environments:
Stable releases — signed from the v* tag ref:
cosign verify ghcr.io/abhigyanpatwari/gitnexus:1.6.2 \
--certificate-identity-regexp '^https://github\.com/abhigyanpatwari/GitNexus/\.github/workflows/docker\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
# Same signature verifies the Docker Hub mirror (identical digest):
cosign verify docker.io/akonlabs/gitnexus:1.6.2 \
--certificate-identity-regexp '^https://github\.com/abhigyanpatwari/GitNexus/\.github/workflows/docker\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The regex pins the certificate identity to this repo's docker.yml workflow
run from a v* tag — rejecting unsigned images, images signed by other
workflows, and images signed from unprotected refs. It is identical for both
registries because both sets of tags were signed at the same digest in one
workflow run.
Release candidates — signed from refs/heads/main (the caller's ref when
publish.yml invokes docker.yml as a reusable workflow):
cosign verify ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1 \
--certificate-identity 'https://github.com/abhigyanpatwari/GitNexus/.github/workflows/docker.yml@refs/heads/main' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
You can also inspect the build provenance and SBOM:
cosign download attestation ghcr.io/abhigyanpatwari/gitnexus:1.6.2 \
--predicate-type https://slsa.dev/provenance/v1
Kubernetes: enforce signatures at admission
For Kubernetes deployments, ship the bundled
ClusterImagePolicy so the
Sigstore policy-controller rejects any GitNexus pod whose
image is not signed by this repo's docker.yml running from a vX.Y.Z tag —
the same identity the cosign verify snippet above pins.
# 1. Install the controller (one-time, cluster-wide)
helm repo add sigstore https://sigstore.github.io/helm-charts && helm repo update
helm install policy-controller -n cosign-system --create-namespace \
sigstore/policy-controller
# 2. Opt your namespace in
kubectl label namespace <your-ns> policy.sigstore.dev/include=true
# 3. Apply the policy
kubectl apply -f deploy/kubernetes/cluster-image-policy.yaml
After this, attempting to deploy an unsigned image — or one signed by anything
other than abhigyanpatwari/GitNexus's docker.yml at a v* tag — fails the
admission webhook before a pod is ever created. This turns the verifiable
signature into an enforced policy, which is the supply-chain control most
clusters actually need.
Files
- Dockerfile.web — builds
gitnexus-sharedandgitnexus-web, then serves the production frontend. - Dockerfile.cli — builds the CLI/server (with its native deps) and runs
gitnexus serve --host 0.0.0.0. - docker-compose.yaml — starts both signed images side by side.
- .env.example — overrides for image names, container names, ports, and the workspace mount.
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, LadybugDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
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.
The Problem GitNexus Solves
Tools like Cursor, Claude Code, Codex, Cline, Roo Code, and Windsurf are powerful — but they don't truly know your codebase structure.
What happens:
- AI edits
UserService.validate() - Doesn't know 47 functions depend on its return type
- Breaking changes ship
Traditional Graph RAG vs GitNexus
Traditional approaches give the LLM raw graph edges and hope it explores enough. GitNexus precomputes structure at index time — clustering, tracing, scoring — so tools return complete context in one call:
flowchart TB
subgraph Traditional["Traditional Graph RAG"]
direction TB
U1["User: What depends on UserService?"]
U1 --> LLM1["LLM receives raw graph"]
LLM1 --> Q1["Query 1: Find callers"]
Q1 --> Q2["Query 2: What files?"]
Q2 --> Q3["Query 3: Filter tests?"]
Q3 --> Q4["Query 4: High-risk?"]
Q4 --> OUT1["Answer after 4+ queries"]
end
subgraph GN["GitNexus Smart Tools"]
direction TB
U2["User: What depends on UserService?"]
U2 --> TOOL["impact UserService upstream"]
TOOL --> PRECOMP["Pre-structured response:
8 callers, 3 clusters, all 90%+ confidence"]
PRECOMP --> OUT2["Complete answer, 1 query"]
end
Core innovation: Precomputed Relational Intelligence
- Reliability — LLM can't miss context, it's already in the tool response
- Token efficiency — No 10-query chains to understand one function
- Model democratization — Smaller LLMs work because tools do the heavy lifting
How It Works
GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:
- Structure — Walks the file tree and maps folder/file relationships
- Parsing — Extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
- Resolution — Resolves imports, function calls, heritage, constructor inference, and
self/thisreceiver types across files with language-aware logic - Clustering — Groups related symbols into functional communities
- Processes — Traces execution flows from entry points through call chains
- Search — Builds hybrid search indexes for fast retrieval
Supported Languages
| 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
Tool Examples
Impact Analysis
impact({target: "UserService", direction: "upstream", minConfidence: 0.8})
TARGET: Class UserService (src/services/user.ts)
UPSTREAM (what depends on this):
Depth 1 (WILL BREAK):
handleLogin [CALLS 90%] -> src/api/auth.ts:45
handleRegister [CALLS 90%] -> src/api/auth.ts:78
UserController [CALLS 85%] -> src/controllers/user.ts:12
Depth 2 (LIKELY AFFECTED):
authRouter [IMPORTS] -> src/routes/auth.ts
Options: maxDepth, minConfidence, relationTypes (CALLS, IMPORTS, EXTENDS, IMPLEMENTS), includeTests, limit (max symbols per depth, default 100), offset (pagination start per depth), summaryOnly (counts and risk only, omits symbol list)
Disambiguation — when several symbols share the target name, impact returns a ranked ambiguous candidate list instead of guessing. Narrow it with target_uid (exact, zero-ambiguity), file_path, or kind (Function, Class, Method, …). From the CLI these are --uid, --file, and --kind, matching gitnexus context:
gitnexus impact get_embeddings # → ambiguous: lists ranked candidates
gitnexus impact get_embeddings --file src/embed.py # → resolves to the one in that file
gitnexus impact get_embeddings --uid "Function:src/embed.py:get_embeddings" # exact
Process-Grouped Search
query({query: "authentication middleware"})
processes:
- summary: "LoginFlow"
priority: 0.042
symbol_count: 4
process_type: cross_community
step_count: 7
process_symbols:
- name: validateUser
type: Function
filePath: src/auth/validate.ts
process_id: proc_login
step_index: 2
definitions:
- name: AuthConfig
type: Interface
filePath: src/types/auth.ts
Context (360-degree Symbol View)
context({name: "validateUser"})
symbol:
uid: "Function:validateUser"
kind: Function
filePath: src/auth/validate.ts
startLine: 15
incoming:
calls: [handleLogin, handleRegister, UserController]
imports: [authRouter]
outgoing:
calls: [checkPassword, createSession]
processes:
- name: LoginFlow (step 2/7)
- name: RegistrationFlow (step 3/5)
Detect Changes (Pre-Commit)
detect_changes({scope: "all"})
summary:
changed_count: 12
affected_count: 3
changed_files: 4
risk_level: medium
changed_symbols: [validateUser, AuthService, ...]
affected_processes: [LoginFlow, RegistrationFlow, ...]
Rename (Multi-File)
rename({symbol_name: "validateUser", new_name: "verifyUser", dry_run: true})
status: success
files_affected: 5
total_edits: 8
graph_edits: 6 (high confidence)
text_search_edits: 2 (review carefully)
changes: [...]
Cypher Queries
-- Find what calls auth functions with high confidence
MATCH (c:Community {heuristicLabel: 'Authentication'})<-[:CodeRelation {type: 'MEMBER_OF'}]-(fn)
MATCH (caller)-[r:CodeRelation {type: 'CALLS'}]->(fn)
WHERE r.confidence > 0.8
RETURN caller.name, fn.name, r.confidence
ORDER BY r.confidence DESC
Wiki Generation
Generate LLM-powered documentation from your knowledge graph:
# Requires an LLM API key (OPENAI_API_KEY, etc.)
gitnexus wiki
# Use a custom model or provider
gitnexus wiki --model gpt-4o
gitnexus wiki --base-url https://api.anthropic.com/v1
# Force full regeneration
gitnexus wiki --force
# Increase the timeout or retries for large codebase or slow LLM providers
gitnexus wiki --timeout <seconds> # LLM request timeout in seconds (default: disabled)
gitnexus wiki --retries <n> # Max LLM retry attempts per request (default: 3)
# Change the language generation for wiki
gitnexus wiki --lang <lang> # Output language for generated documentation (e.g. english, chinese, spanish, japanese)
The wiki generator reads the indexed graph structure, groups files into modules via LLM, generates per-module documentation pages, and creates an overview page — all with cross-references to the knowledge graph.
Tech Stack
| Layer | CLI | Web |
|---|---|---|
| Runtime | Node.js (native) | Browser (WASM) |
| Parsing | Tree-sitter native bindings | Tree-sitter WASM |
| Database | LadybugDB native | LadybugDB WASM |
| Embeddings | HuggingFace transformers.js (GPU/CPU) | transformers.js (WebGPU/WASM) |
| Search | BM25 + semantic + RRF | BM25 + semantic + RRF |
| Agent Interface | MCP (stdio) | LangChain ReAct agent |
| Visualization | — | Sigma.js + Graphology (WebGL) |
| Frontend | — | React 18, TypeScript, Vite, Tailwind v4 |
| Clustering | Graphology | Graphology |
| Concurrency | Worker threads + async | Web Workers + Comlink |
Roadmap
Actively Building
- LLM Cluster Enrichment — Semantic cluster names via LLM API
- AST Decorator Detection — Parse @Controller, @Get, etc.
- Incremental Indexing — Only re-index changed files
Recently Completed
- Constructor-Inferred Type Resolution,
self/thisReceiver Mapping - Wiki Generation, Multi-File Rename, Git-Diff Impact Analysis
- Process-Grouped Search, 360-Degree Context, Claude Code Hooks
- Multi-Repo MCP, Zero-Config Setup, 14 Language Support
- Community Detection, Process Detection, Confidence Scoring
- Hybrid Search, Vector Index
Security & Privacy
- CLI: Everything runs locally on your machine. No network calls. Index stored in
.gitnexus/(gitignored). Global registry at~/.gitnexus/stores only paths and metadata. - Web: Everything runs in your browser. No code uploaded to any server. API keys stored in localStorage only.
- Open source — audit the code yourself.
Acknowledgments
- Tree-sitter — AST parsing
- LadybugDB — Embedded graph database with vector support (formerly KuzuDB)
- Sigma.js — WebGL graph rendering
- transformers.js — Browser ML
- Graphology — Graph data structures
- MCP — Model Context Protocol