* fix(group)!: remove the matching cascade that was advertised but never built `gitnexus group create` wrote `matching.bm25_threshold` and `matching.embedding_threshold` into every generated group.yaml, and no matcher ever read either one. That was not the whole of it — an entire feature surface described a BM25/embedding cascade that does not exist: - `matching.bm25_threshold` / `matching.embedding_threshold` — parsed, persisted, unread - `detect.embedding_fallback` — defaulted and templated, unread - `MatchType` declared `'bm25' | 'embedding'`; both variants unreachable - `SyncOptions.skipEmbeddings` — declared in sync.ts and never read - `gitnexus group sync --skip-embeddings` — accepted, threaded through GroupService, ignored - CLI help in en and zh-CN promised "Exact + BM25 only (no embedding fallback)" - the MCP `group_sync` schema exposed `skipEmbeddings`, described as "Exact + BM25 only (Demo PR: same as default exact path)" `sync.ts` imports exactly `buildProviderIndex`, `runExactMatch` and `runWildcardMatch`, and the printed cascade has one stage. An operator whose links do not match reaches for those thresholds first, and turning either knob changes nothing — config that silently does nothing is how people conclude a feature is broken. Evidence that the cascade should be deleted rather than implemented, from a real backend/frontend pair: of 165 consumer contracts, 149 link exactly and 16 do not. Nine of the sixteen are third-party APIs (Google OAuth, Apple public keys, PostHog, image annotation) with no in-group provider by construction — similarity matching cannot recover them, it can only invent false links. Two are verb mismatches: the frontend calls `POST /links` and `GET /links/check-exists` while the backend declares `GET /links` and eleven other `/links/*` routes but neither of those, so a fuzzy path match would link a POST consumer to a GET provider. The rest are path-extraction artifacts. Roughly none of the sixteen would be correctly recovered, and several would be actively mis-linked. BREAKING CHANGE: `gitnexus group sync --skip-embeddings` and the MCP `group_sync` `skipEmbeddings` parameter are removed. Both were accepted and ignored, so no behavior changes — but a script passing the flag now fails with `unknown option` instead of being silently misled. Existing group.yaml files keep loading: the removed keys are simply no longer part of the schema, and a regression test pins that a legacy config carrying all three still parses. Closes #3006 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(group)!: honour --exact-only, drop inert --allow-stale, report every matching stage Addresses the review findings on #3020, all of which are the same defect the PR itself is about: group-sync surface that describes behaviour the pipeline does not have. `exactOnly` was inert in exactly the way `skipEmbeddings` was — declared on `SyncOptions`, threaded through the CLI and the MCP tool, and read by nothing — and strictly worse, because the stage it promised to suppress DOES run and DOES write `matchType:'wildcard'` links into contracts.json and the bridge, which `group impact` and cross-repo `trace` then traverse. It is now honoured rather than deleted: unlike the never-built BM25/embedding stages, the stage it names exists, so the flag describes a real choice. The substituted result is `{ matched: [], remaining: unmatched }`, not an empty result — `wildcard.remaining` IS `SyncResult.unmatched`, so skipping the stage has to leave its input unmatched rather than dropping it from the count an operator reads. `allowStale` had no such stage to gate: `syncGroup` emits no stale warning at any point (the `checkStaleness` call lives in `groupStatus`, a different path), so it is removed under the same rationale as `skipEmbeddings`. `group sync` now prints every matching stage instead of `exact` alone. The old block printed a `Matching cascade:` header and counted only exact links while the next line reported `result.crossLinks.length` — which also includes `manifest` and `wildcard` — so for any group with those the two numbers disagreed with nothing on screen explaining why. Counting is an exhaustive `Record<MatchType, number>`, so a new MatchType fails the build here instead of going silently uncounted, and reads through `?? 0` so a legacy registry carrying a removed matchType prints an honest count rather than `NaN`. Also: the MCP `group_sync` description no longer omits the wildcard stage that always runs, `exactOnly`'s description no longer refers to a "cascade", and bench/cross-repo-trace/verify.mjs no longer generates the removed threshold keys into a fresh group.yaml. Tests: `sync-exact-only.test.ts` pins both directions of the gate (mutation-verified: removing the gate, or returning `remaining: []`, both go red). `group-tools.test.ts` pins that the MCP schema dropped `skipEmbeddings` and kept `exactOnly`. `group-cli.test.ts` pins that both removed flags are rejected, with `--exact-only` as an accepted-flag control. `config-parser.test.ts` now pins that legacy keys are PRESERVED (measured, not assumed) rather than only that parsing does not throw. The type narrowing's fallout in test files is cleared: `tsc -p tsconfig.test.json` is 987 errors at head against 987 measured on origin/main, with the two error sets identical — zero net, zero new, zero masked. Verification: `tsc --noEmit` exit 0; prettier clean; eslint 0 errors (2 warnings, both pre-existing on base); 69 test files / 1169 tests green across test/unit/group, test/integration/group, tools, cli-i18n and cli-index-help. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(group): reject malformed and retired group_sync parameters (U1) `GroupService.groupSync` read `exactOnly` off an untyped MCP payload with `Boolean(params.exactOnly)`. While the flag was inert that coercion was harmless; now that it gates the wildcard matching stage, the string "false" -- a routine shape for an LLM caller emitting JSON -- is truthy, so a caller that asked to KEEP wildcard matching got it suppressed and a registry with fewer cross-links persisted to disk. The opposite of the request, written down. Validate instead of coercing, at the service boundary: the MCP SDK does not enforce a tool's advertised inputSchema and `callTool` is reachable directly, so this method is the real gate. The validator mirrors `validateImpactMode`'s `{ value } | { error }` shape -- the established idiom for this boundary, and the one groupSync's other guards already return through. Also refuse `skipEmbeddings` and `allowStale` by name. The CLI rejects them outright because commander errors on an unknown option; the MCP path accepted and silently dropped them, so an agent working from a cached tool schema was never told. Removing them took away discoverability, not acceptance. Both guards run before the group is read off disk, so a rejected call performs no work. Every test asserts the sync did NOT run -- an error string alone cannot distinguish "refused" from "refused but synced anyway". The tool description gains the validation note AFTER the registryOutcome paragraph: `tools.test.ts` slices that description by ordinal position of the 'preserved' / 'superseded' / 'no-prior-registry' literals, so appending past all three leaves those slices intact (verified, 44/44). tsc clean; 1039/1039 group unit tests pass. * fix(group): record the matching stages a sync was told to skip (U2) An `--exact-only` sync wrote a contracts.json and bridge with fewer cross-links and nothing recorded that the wildcard stage had been suppressed by request. `group_impact` and cross-repo `trace` read that registry as authoritative, so a narrowed graph was indistinguishable from a complete one -- and because `group_sync` is MCP-exposed, one agent call durably narrowed the shared answer for every later reader with no signal at all. Add `suppressedMatchStages` to ContractRegistry and SyncResult, following the `unreadableRepos` tri-state end to end: absent means a registry written before the field existed, `[]` is the measurement "this run suppressed nothing", and a populated list names the stages. The writer always emits it, because omitting the empty case is what made "measured, none" unreachable for `unreadableRepos`. Two properties that are easy to get backwards, and are why the split matters: - SyncResult carries the marker on EVERY outcome. The sync genuinely did skip the stage whatever happened to the file afterwards, and the CLI summary (U3) renders from this rather than re-deriving it from the caller's options. - The PERSISTED registry stamps it only on the `written` outcome. The preserve path re-writes `{ ...prior }`, so a carried-forward registry keeps the marker of the sync that actually produced its contracts instead of being relabelled with this run's request. That holds by construction: the registry literal carrying the field is only reachable on the written path. `loadContractRegistryResilient` gains an explicit line, because it rebuilds the envelope field by field with no spread of the parsed root -- a new on-disk field is silently dropped unless named there. Its reader is `recordedMatchStages`, not the existing `recordedRepoList`: that one validates `string[]`, which is right for repo names and one notch too weak here. This repo has already retired MatchType members ('bm25', 'embedding'), so a stale value on disk is a real shape, and dropping non-members keeps an unknown stage name from reaching a caller typed as a live one. Surfaced on `group_contracts` and on `group_sync`'s own return -- deliberately kept separate from the truncated/truncationReason/riskEpistemic triple. That triple reports limits a run hit by accident, whose remedy is to fix the repo; a suppressed stage was asked for, and its remedy is to re-sync without the flag. Conflating them would tell an agent to retry something that returns identically. tsc clean; 1043/1043 group unit tests pass. * fix(group): name a skipped matching stage as skipped, and pin it (U3, U4) Two facts were printing as the same line. `wildcard: 0 cross-links` meant both "the stage ran and matched nothing" and "the stage never ran because you passed --exact-only" -- the same conflation this summary block was introduced to remove one line up, reintroduced by the flag that made the block necessary. Render a suppressed stage as `skipped (--exact-only)`, driven by the sync's own `suppressedMatchStages` rather than by `opts.exactOnly`. The renderer reports what the sync did, not what the caller asked for, so it stays correct on the outcomes where the run ended without writing a registry -- which is where the summary is least legible and a re-derivation from the options would have been wrong. Also drops the `?? 0` fallback and the comment justifying it. The comment claimed a legacy registry could carry a retired matchType into this loop. It cannot: `syncGroup` returns a freshly computed `crossLinks` array on every outcome, and even on the preserve path the prior links go to disk while the fresh array is returned. The code was harmless; the stated reason was false, and a comment that explains an unreachable path is worse than no comment. U4 pins both halves through the CLI. A manifest fixture is sufficient: the stage counts must sum to the total on the `Wrote contracts.json (…)` line, and the skipped rendering does not need a stage to have matched anything, because --exact-only records the suppression whatever the fixture holds. That is why this coverage did not need indexed gRPC/Thrift fixture repos. Verified by mutation, not assertion: removing the skipped-rendering branch turns `names a stage it was told to skip as skipped` red and leaves the other 22 green. A control case pins the opposite direction -- the same group without the flag still reports the stage as zero -- so `skipped` cannot be printed unconditionally and pass. U3 and U4 land together: the test has no value without the renderer, so one commit keeps a revert clean. Both depend on U2, which introduced the field they read. tsc clean; 1066/1066 across the group unit and CLI integration suites. * fix(group): make every description of --exact-only match what it does (U5) Two descriptions this branch wrote or touched still misstated behavior. The MCP `exactOnly` description carries "Manifest links still apply." The CLI help and both locale strings, rewritten in the same commit, omit it -- so the surface most operators read understated what still runs. Manifest cross-links are computed before the gate and are genuinely unaffected by the flag, so the caveat is the accurate half and the CLI now says it too. The `group_sync` tool description opened with "extract HTTP contracts". That clause was carried forward byte-identical while only the trailing cross-linking half was rewritten, and it is wrong: the detect config has six non-HTTP extraction toggles, and this branch's own new test fixture is Thrift. `help-i18n.ts` is deliberately untouched. It maps an option to its translation key and that key already exists; only the commander string and the two locale values carry text, so a text-only change does not reach it. The tool-description edit sits ahead of the registryOutcome paragraph, leaving the relative order of the 'preserved' / 'superseded' / 'no-prior-registry' literals intact -- `tools.test.ts` slices that description by their positions. tsc clean; 64/64 across the locale-parity, help-registration, tool-schema and group-tool suites. * fix(group)!: remove max_candidates_per_step and shared_libs (U6) Both keys were declared, defaulted, written into every generated group.yaml, and read by nothing -- the same three-station dead surface this PR removed for bm25_threshold, embedding_threshold and detect.embedding_fallback. Every other DetectConfig field gates a real extractor in sync.ts; shared_libs gates nothing, because 'lib' contracts come only from the operator-declared manifest extractor. MatchingConfig reaches matching.ts solely through buildNoisyContractFilter, which reads exclude_links_paths and exclude_links_param_only_paths and nothing else. Existing group.yaml files keep loading and keep their keys. parseGroupConfig spreads the raw block over its defaults, so a key the schema no longer knows about survives into the returned config -- which matters because `group add` and `group remove` round-trip the operator's file through loadGroupConfig -> yaml.dump -> write, so anything the parser dropped would be deleted from their checked-in file. The legacy-config test now pins both keys in the same cast form as its three siblings, and the fixture carries shared_libs so that assertion is not vacuous. Two stations that are easy to miss and are swept here: - gitnexus/bench/cross-repo-trace/verify.mjs GENERATES a fresh group.yaml. It is not a preserve-path fixture, so "leave YAML fixtures alone" does not cover it; the repo has two generators and both are updated. It is a .mjs file outside tsconfig's include, so no type gate would have caught it. - config-parser.test.ts asserted the removed default at runtime, which vitest DOES run. That assertion is gone from the defaults case (the key no longer has a default) and re-formed as a preserve assertion in the legacy case. Verification gate, corrected: "zero net new errors against origin/main" would have measured the whole branch delta and been red through no fault of this commit. Measured instead against the branch tip immediately before it -- tsc -p tsconfig.test.json --noEmit reports 989 before and 989 after. Twenty-four typed-literal sites across ten test files, none of them CI-gated, plus the two runtime sites above which are. Note the deliberate side effect: removing a key from the defaults also stops the group add round-trip from re-adding it to a file that never carried it. Nothing in src reads either key, so no behavior changes. BREAKING CHANGE: `matching.max_candidates_per_step` and `detect.shared_libs` are no longer part of the group.yaml schema and are no longer written into generated templates. Existing files carrying them continue to parse and retain them. src tsc clean; 1189/1189 across the group unit, group integration, locale-parity, help-registration and tool-schema suites. * docs(group): map PR #3020 review findings to the commits that close them Retitles the ledger to hold one section per reviewed PR and adds #3020's ten findings. Two things are stated rather than claimed away: `abda0d041` closes three findings because they are one code block plus the test that pins it, and the suppressed-stage marker is a coupled set because the renderer consumes the field the earlier commit introduces. Also records what is NOT closed here -- the PR description's false claim about `max_candidates_per_step` lives outside this branch. * refactor(group): apply simplify-pass findings Four cleanup agents (reuse, simplification, efficiency, altitude) over this run's diff. Efficiency was clean. The rest found five things worth fixing, two of which were real gaps rather than style. `recordedMatchStages` filtered unknown values instead of rejecting the list. That inverted the tri-state on the one field built to prevent exactly this conflation: a stale `['bm25']` -- the scenario its own comment cites as the motivation -- survived as `[]`, which on this field MEANS "measured, nothing was suppressed". A confident clean answer manufactured from a value we could not read. Now all-or-nothing, matching `recordedRepoList`. `gitnexus group contracts` showed nothing after an exact-only sync. The human renderer destructures a fixed field list and gates its incompleteness warning on `truncated`, so the marker reached the MCP payload and the JSON output but not the listing an operator actually reads. It now warns, separately from the `truncated` warning, because the remedies differ: one says fix the repo, this one says re-run without the flag. `verbose` was still coerced with `Boolean()` in the same call whose tool description this branch changed to promise "PARAMETERS ARE VALIDATED". Validated now, and added to the tool schema -- it was read by the backend and advertised nowhere. Reuse: the thrift wildcard-matchable pair existed twice, near-verbatim, in `sync-exact-only` and `registry-suppressed-stages`. Both now call a shared `makeWildcardPair` fixture, so the shape `runWildcardMatch` fires on is defined once. Simplification: dropped a `Set` built per sync over a list that only ever holds zero or one entries; iterating `Object.keys(STAGE_COUNTS) as MatchType[]` also keeps the exhaustiveness the `Record` was built for, which `Object.entries` had discarded. Deliberately not done, with reasons: a schema-driven unknown-parameter layer at the MCP chokepoint (five parameters are read by backends and declared in no schema, so a strict layer rejects working calls today, and it cannot produce the "was removed" message finding 3 is about); folding the marker into `GROUP_IMPACT_TRUNCATION_REASONS` (reverses a recorded plan decision and the bridge scope is an open question for the maintainer); a per-stage suppression cause `Record` (no second suppressor exists -- speculative); collapsing the six `detect` extractor branches into a table (a real generalization, but a refactor outside this diff); and converging an untouched pre-existing CLI test onto the new manifest helper (it captures a value the helper does not return, so the change risks more than the duplication costs). tsc clean; eslint 0 errors (1 pre-existing warning); 1085/1085. * docs(group): remove REVIEW-FINDINGS-MAP.md Removes the findings-to-commits ledger from the source tree. Note for anyone reading this in history: the file was introduced on main by #3012 and carried that PR's findings map; this branch had appended a #3020 section. Deleting it drops both. #3012's content is recoverable with `git show 2c0fb7753:gitnexus/src/core/group/REVIEW-FINDINGS-MAP.md`. * fix(group): stop cross-repo impact and trace claiming a narrowed graph is complete Closes the half of the suppressed-stage finding that was deferred. The reviewers were right that deferring it was the weak point: the motivating harm was named as `group_impact` and cross-repo `trace` traversing a graph missing real edges, and those were exactly the surfaces left uncovered. The deferral rested on an assumption that does not hold. "It is already blind to this, so we do not make it worse" is false: `--exact-only` was inert before this PR, so the number of narrowed registries in the world goes from zero to nonzero exactly when this lands. The blindness was harmless only while narrowing was impossible. And silence there is not neutral -- `cross-impact.ts` documents `truncated: false` as an affirmative completeness claim, so those tools were about to start asserting a complete answer over a knowingly short graph. `suppressedMatchStages` now rides the bridge the same way `unreadableRepos` does: persisted in meta.json (no BRIDGE_SCHEMA_VERSION bump -- meta fields have this precedent), read back all-or-nothing, and carried across the preserve path through `refreshPreservedBridgeMeta`'s diagnostics so a preserved bridge keeps the marker of the sync that actually built it. `crossRepoCompleteness` folds it in, which is what makes this one change reach all three surfaces -- that function is by design the ONE computation behind the truncation triple. Precedence is explicit: an unreadable or unaccounted repo outranks a suppressed stage, because it is the more serious structural gap and its remedy has to be the one reported. `'suppressed-stage'` is a new member of the truncation-reason union rather than a reuse of `'incomplete-sync'`. The earlier decision not to touch that union was about not conflating remedies -- telling an agent to repair a repo that read fine, for a narrowing it requested. A distinct member preserves that reasoning while letting the answer stop claiming completeness, which is what reusing the existing member would have destroyed. The union's guard test did its job: adding a member failed the check that every reason is explained on the agent-facing surface, so the impact tool description now names this one and its distinct remedy (re-run WITHOUT the flag; nothing failed to read). `group status` and its CLI renderer surface it too, on the populated case only -- absent is a registry predating the field and empty is the ordinary clean sync; neither earns a line. Deliberately still not done, and why: a repo-wide unknown-parameter layer for every MCP tool. Five parameters are read by backends and declared in no schema (`subgroupExact`, `unmatchedOnly`, `showClusters`, `showProcesses`, and `verbose` until this branch declared it), and three tools dispatch with no schema entry at all, so a strict layer rejects working calls until each is reconciled. That reconciliation is the work; the layer is the cheap part. It also cannot produce the "was removed and is no longer accepted" message the retired-parameter guard exists to give. tsc clean; eslint 0 errors (2 pre-existing warnings); 1159/1159 across the group unit, group integration and tool-schema suites. * fix(group): make the suppressed-stage signal actually reach its readers Applies the mechanical findings from the code review of the previous commit. That commit claimed cross-repo impact and trace stop reporting a narrowed graph as complete. Trace did; impact did not, and two operator-facing messages said something false. Four reviewers plus the cross-model pass converged on the same two defects, and the untested seams were exactly where they were. `runGroupImpact` recomputed the truncation reason and hardcoded its fallback, so it could never emit 'suppressed-stage' -- the value the previous commit added to the union and documented in the tool description. Every narrowed-but-readable bridge was reported as 'incomplete-sync', telling the caller to repair a repo that read fine. It now propagates the bridge's own reason, as cross-trace.ts already did. The preserve path stamped this run's request onto an older bridge. When no repo can be read the database and registry are kept from an earlier sync, so meta.json has to keep describing that sync; instead `{ ...existing, ...diagnostics }` overwrote its marker, leaving contracts.json, meta.json and bridge.lbug describing three different runs. Currently masked by unreadable-repo precedence, one loosened condition from a live wrong verdict. `group contracts` printed "the last sync did not record which repos it could read" after any exact-only sync: truncated was set with both repo lists empty, so the message fell through to the wrong branch. It is now gated on the reason, not the flag. `group impact` likewise blamed the local walk for a floor the flag caused. The tri-state reader is now defined once, in the leaf module whose own comment says it exists so this exact duplication cannot recur -- it had been copied into bridge-db.ts within one commit of that comment being true. Both agent-facing descriptions now name the field. The previous commit added it to three payloads and documented it on none. Tests cover what shipped green: the preserve path for both artifacts (verified by mutation -- reintroducing the stamp turns exactly one test red), and the reason's REACHABILITY. The existing guard only asserted each reason is described, which is why a documented-but-unemittable value passed it. Also corrects a comment that said the marker is deliberately not folded into the truncation triple. True when written; false one commit later. tsc clean; eslint 0 errors; 1163/1163 across the group unit, group integration and tool-schema suites. * fix(group): drop verbose from the MCP surface, fail a superseded bridge closed Two maintainer-directed findings from the review. verbose is removed from the group_sync MCP schema and from GroupService, and kept on the CLI. The parameter never did what either description claimed: the gates emit workspace-dependency discovery stats and one aggregate manifest line, not "each cross-link". Worse, they emit them through the server's logger, which an MCP caller cannot read at all -- so advertising it introduced precisely the kind of knob this PR exists to delete, in the PR that deletes them. SyncOptions keeps the field and the CLI keeps --verbose, because a CLI user really can see that output; its help now says "Show additional sync diagnostics", which is what it shows. It was added to the MCP schema earlier in this same PR, so there is no published compatibility burden in taking it back out. A caller that still sends it is ignored rather than refused: it was never a documented parameter, and the retired-name guard is reserved for ones this tool actually withdrew. The second fixes a split-brain the completeness work made materially worse. When contracts.json commits and the bridge write then fails, the previous database stays in place describing an EARLIER sync. Until now it kept vouching for itself, so group_impact could traverse the superseded graph and call its answer complete while group_contracts reported the advanced registry -- two public surfaces making contradictory epistemic claims out of one sync. That was tolerable when the disagreement was about counts. It is not, now that suppressed-stage makes completeness a correctness property. markBridgeProvenanceUnknown withdraws the claim without touching the database: bridgeMetaMatchesFile already gives provenanceUnknown highest precedence and refuses to vouch for the pair, so cross-repo answers downgrade to a floor until a sync succeeds. Deliberately not a re-stamp -- the metadata still describes the database it was written for, and saying otherwise recreates the mis-pairing the preserve path avoids. Deliberately not a delete -- the old graph is still worth having as a floor, it just stops being called complete. Best-effort, because it runs inside a failure handler and must not replace a reported bridge failure with an unrelated one; the warning now states which of the two happened. Shared registry+bridge generation identity is the architectural fix and is deliberately NOT attempted here. This is the PR-sized containment. Verified by mutation, both directions: neutering the withdrawal turns the new test red, and a control pins that a healthy sync does not withdraw provenance -- otherwise every successful run would report its own answers as a floor. tsc clean; eslint 0 errors; 1185/1185. * refactor(group): apply simplify-pass findings Four cleanup agents over the last five commits. Efficiency was clean and traced why: the containment helper is failure-path only, the reason ternary sits after the fan-out loop, and the tri-state readers run once per artifact read. The strongest finding was one the diff itself proved. `refreshPreservedBridgeMeta` enforced the never-persisted rule for `repoListsUnreadable` and `pairedWithDatabase` with two deletes in its own body, under a comment noting it was the only code that read metadata and wrote it back. That held exactly as long as there was one such caller. `markBridgeProvenanceUnknown` made it two, and inherited nothing. The strip now lives in `writeBridgeMeta`, so every writer gets it and no future one can forget; `pairedWithDatabase` is the dangerous one, because persisted it tells every later reader the pair was verified when nothing verified it. `group impact` still printed "fan-out stopped early" whenever `truncatedRepos` was non-empty — but the bridge's incomplete repos are unioned into that list even when zero crossings were attempted, so a structural gap was reported as a runtime one, with the only working remedy omitted. That is the same false-cause shape the contract listing was re-gated for one commit ago, left live one command over because the new reason was bolted in front of the old branch rather than replacing the thing it branched on. Now keyed on the reason. The `?? 'incomplete-sync'` arm in cross-impact was unreachable: reaching it needs `truncated` true with all three of its inputs false, which `truncated = runtimeTruncated || bridge.truncated` forbids. Flattened. Also: a `recordedMatchStages` insert had split `crossRepoCompleteness` from its own JSDoc; one new test was a strict subset of another; and the bridge-failure warning interleaved concatenation with a mid-chain ternary. The new invariant assertion was caught being VACUOUS by mutation before it shipped — seeded with a valid repo list, `readBridgeMeta` never sets the reader-only field, so it passed with or without the strip. The fixture now seeds an unreadable list, and both it and the pre-existing assertion go red when the strip is removed. Deliberately skipped, with reasons: a shared `firstTruncated` fold over `TruncationFields` (the right altitude, but it changes cross-trace's return assembly and that surface separately documents a 'timeout' rung it cannot emit — a behavior change, not a cleanup); a reason-keyed `explainFloor` helper across all four CLI renderers (real, but a four-site refactor); narrowing the persisted stage vocabulary to a `SuppressibleStage` alias (would be undone by the very extension the field was modelled as a list to allow); moving `verbose` to `logger.debug` and deleting `SyncOptions.verbose` (the maintainer explicitly directed keeping both); and merging the two tri-state readers behind a predicate (they are adjacent in one file now, so a tightening applies to both by inspection — the duplication the comment warned about was cross-FILE). tsc clean; eslint 0 errors; 1164/1164. * fix(group): address gitnexus-check findings Seven bot comments across two review rounds; five distinct after dedup. Four were valid and are fixed, two were already resolved by later commits the bot had not seen. The validator could throw from its own error path. `JSON.stringify` is the right renderer there — it is what distinguishes the string "false" from the boolean, which is the entire point of the message — but it throws on a BigInt and on a cyclic object. So a validator promising a structured `{ error }` instead rejected, and `callTool` is reachable directly, so neither input is hypothetical. Guarded, keeping the distinction and falling back for the shapes that cannot serialize. An unreadable suppression record read as "nothing was suppressed". `recordedMatchStages` is all-or-nothing by design, so garbage collapses to `undefined` — and the consumer treated `undefined` as an empty measurement, throwing that safety away and reporting a registry it could not parse as complete. Present-but-unreadable now forces the floor, while absent stays legitimate: a registry written before the field existed has no opinion and should not be dragged to a floor for it. Two test-side findings, both real and both invisible to CI because `tsconfig.json` is src-only. Three `mock.calls[0][1]` accesses did not type-check against a zero-arg mock, and four assertions read `truncationReason` / `riskEpistemic` straight off `CrossRepoCompleteness`, which is a discriminated union carrying them on one arm. Also removed a `StoredContract` import that went dead when those fixtures moved to `makeWildcardPair`. Worth recording: U6 set a test-config gate at 989 errors and later commits walked it to 994 without anyone re-measuring — the bot caught three of the five. Now 987, below the original baseline. Already fixed, not by this commit: the preserve-path stamp the bot flagged against |
||
|---|---|---|
| .agents/plugins | ||
| .claude | ||
| .claude-plugin | ||
| .cursor | ||
| .devcontainer | ||
| .gemini/commands | ||
| .github | ||
| .history/gitnexus | ||
| .husky | ||
| .sisyphus/drafts | ||
| deploy/kubernetes | ||
| docs/plans | ||
| Documentation | ||
| 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 | ||
| .gitleaksignore | ||
| .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 | ||
| render.yaml | ||
| RUNBOOK.md | ||
| SECURITY.md | ||
| skills.mdm | ||
| swift-ingestion-gaps.md | ||
| TESTING.md | ||
| type-resolution-roadmap.md | ||
| type-resolution-system.md | ||
GitNexus (Akon Labs)
⚠️ 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.
The 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 MCP tools so AI agents never miss code.
💬 Discord · 🌐 Web UI · 🏢 Enterprise (SaaS & self-hosted)
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 — a knowledge graph tracks every relationship, not just descriptions.
TL;DR: The CLI + MCP makes your AI agent 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. The Web UI is a quick way to chat with any repo in the browser.
Quick Start
# 1. Index your repo (run from repo root)
npx gitnexus analyze
# 2. Connect your editors (one-time, auto-detects Claude Code, Cursor, Codex, …)
npx gitnexus setup
That's it. analyze indexes the codebase, installs agent skills, registers Claude Code hooks, and creates AGENTS.md / CLAUDE.md context files — all in one command. setup writes the MCP config so your AI agent can use the graph.
Install problems? npm 11 crash · slow cold install · no C++ toolchain
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.
Fastest MCP startup: install globally (
npm i -g gitnexus) before runninggitnexus setup— this writes an absolute-path MCP config that bypassesnpxentirely. On a cold cache, annpx-based MCP install can exceed Claude Code'sMCP_TIMEOUTdefault (~30s).
No C++ toolchain? 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 languages won't be parsed, but install completes in seconds withoutpython3/make/g++. Strict=1only — any other value falls through to the rebuild.
Behind an HTTP proxy / regional firewall?
onnxruntime-node's postinstall downloads optional CUDA binaries fromapi.nuget.organd ignoresHTTP_PROXY/HTTPS_PROXY(#2370). The embedding stack is an optional dependency, so a failed download no longer breaks the install — and it self-heals: the firstgitnexus analyze --embeddings(orgitnexus embeddings install) fetches the stack through your npm registry config (mirrors/proxies apply, no NuGet) into~/.gitnexus/embedding-runtime(override withGITNEXUS_EMBEDDING_RUNTIME_DIR). The on-demand prefix needs Node withmodule.registerHooks(≥ 22.15 on 22.x, ≥ 23.5 on 23.x); on older Node, keep the stack in the install itself withONNXRUNTIME_NODE_INSTALL=skip npm install -g gitnexus(works on every supported Node).
About
tree-sitter-kotlin: like Dart/Proto/Swift, Kotlin is a vendored grammar (undergitnexus/vendor/tree-sitter-kotlin). Upstream ships source only (no prebuilt binaries), so GitNexus cross-builds the platform prebuilds itself (via thebuild-tree-sitter-prebuildsGitHub Actions workflow) and vendors them — the same uniform pipeline used for Dart, Proto, and Swift.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.
Deploy to Render
Deploy GitNexus in one click:
The Blueprint creates two services. gitnexus-server runs gitnexus serve as a private service: no public URL, reachable only over Render's private network, with a persistent disk for indexes and cloned repos. gitnexus-web is the public one. It serves the UI and reverse-proxies /api/* to the server, so the browser talks to a single origin.
At the Blueprint's defaults this runs about $35/month: $25 for the server's standard instance, $7 for the web service's starter instance, and $2.50 for the 10 GB disk. See Render's pricing for other plans.
The deploy generates an access token, and the UI asks for it on first use:
- Open the
gitnexus-webservice in your Render dashboard. - Copy
GITNEXUS_SERVE_AUTH_TOKENfrom its Environment tab. - Load the site and paste the token into the prompt (or the settings panel).
Every /api/* request carries that token as a header, and the proxy answers 401 without it. The browser keeps it in sessionStorage, so a new tab asks again. To rotate it, edit the environment variable and redeploy.
The proxy strips Origin before forwarding, so the server's CSRF guard does nothing for proxied traffic; it passes Origin-less requests through by design. The token is the only control on this deploy, not a second layer behind the guard. Anyone holding it can read every indexed repo. See SECURITY.md.
Indexing is memory-bound. If gitnexus-server runs out of memory on a large repo, raise its plan, which sets available RAM: standard is 2 GB, pro is 4 GB. Raise sizeGB only if the disk fills with clones and indexes.
Two Ways to Use GitNexus
| CLI + MCP (recommended) | 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.
Why a Knowledge Graph?
Tools like Cursor, Claude Code, Codex, Cline, Roo Code, and Windsurf are powerful — but they don't truly know your codebase structure. So this happens:
- AI edits
UserService.validate() - Doesn't know 47 functions depend on its return type
- Breaking changes ship
Traditional Graph RAG gives the LLM raw graph edges and hopes 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 — the 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 the tools do the heavy lifting
What Your AI Agent Gets
17 MCP tools (15 per-repo + 2 group)
| 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 |
Per-repo tools take an optional
repoparameter (omit it when only one repo is indexed) and an optionalbranchfor indexes pinned withgitnexus analyze --branch. Omittingbranchqueries the workspace index, which follows your checked-out working tree — switching branches and re-runninggitnexus analyzeupdates it incrementally.explainandpdg_queryneed an index built withgitnexus analyze --pdg.
Resources for instant context
| Resource | Purpose |
|---|---|
gitnexus://repos |
List all indexed repositories (read this 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 |
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 |
Agent skills installed to .claude/skills/ and .agents/skills/ (if .agents/ exists) 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
- 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 (
--pdgindex) - Taint Analysis — source→sink data-flow findings (
--pdgindex) - Plan (
/gitnexus-plan) — implementation-ready engineering plans backed by the graph and PDG slices - Work (
/gitnexus-work) — executes a plan as impact-checked,detect_changes-gated atomic commits - Review (
/gitnexus-review) — graph-backed review of a PR, branch, range, or local diff, with taint pass and per-domain expert lenses - LFG (
/gitnexus-lfg) — the full pipeline: plan → user gate → work → review
Repo-specific skills — run gitnexus analyze --skills and GitNexus detects the functional areas of your codebase (via Leiden community detection) and generates each one as a direct project skill under .claude/skills/gitnexus-area-<name>/. Each skill describes a module's key files, entry points, execution flows, and cross-area connections, and is regenerated on each --skills run to stay current.
When a repo contains an .agents/ directory, the standard and generated skills are also mirrored to .agents/skills/ (e.g. .agents/skills/gitnexus-cli/, .agents/skills/gitnexus-area-<name>/) so agents that read repo-local .agents/skills/ (like Codex) stay in sync.
Editor Setup
gitnexus setup auto-detects your editors and writes the correct global MCP config. Run it once. To configure only selected integrations, pass --coding-agent/-c with a comma-separated list, e.g. gitnexus setup -c cursor,codex.
| 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 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.
Manual MCP configuration (if you prefer not to run 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
Or via ~/.codex/config.toml (system scope) / .codex/config.toml (project scope):
[mcp_servers.gitnexus]
command = "npx"
args = ["-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
/hooksbefore they run. Pick one install route (gitnexus setup -c codexor the plugin): plugin hooks load alongside~/.codex/hooks.json, so installing both can fire duplicate hooks per tool call.
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"]
}
}
}
CodeBuddy (Tencent) — priority chain, edit the first non-empty file that exists: ~/.codebuddy/.mcp.json (recommended) → ~/.codebuddy/mcp.json (deprecated) → ~/.codebuddy.json (legacy). CodeBuddy reads only the first existing file, so adding servers to a higher-priority file than the one currently in use would hide the servers below it. Create ~/.codebuddy/.mcp.json only if none exist:
{
"mcpServers": {
"gitnexus": {
"command": "npx",
"args": ["-y", "gitnexus@latest", "mcp"]
}
}
}
Qoder (Alibaba) — ~/.qoder.json:
{
"mcpServers": {
"gitnexus": {
"command": "npx",
"args": ["-y", "gitnexus@latest", "mcp"]
}
}
}
MCP read-only mode
Set GITNEXUS_MCP_READ_ONLY=1 before starting the MCP server to expose only the proven single-repository read surface. Raw cypher, rename and group tools, group routing, and group resources are omitted from discovery and rejected before backend dispatch. Tool descriptions and generated setup/context resources are scrubbed so they do not recommend unavailable routes.
The default is unchanged when the variable is unset or 0. Any other value fails server startup rather than silently weakening the policy.
MCP repository policy
Set GITNEXUS_MCP_ALLOWED_REPOS to a comma-separated list of canonical registry names or absolute indexed paths. Entries are trimmed, resolved against the registry, and deduplicated at startup. When exactly one repository is allowed it becomes the implicit default; when several are allowed, callers must select one unless GITNEXUS_MCP_DEFAULT_REPO is also set.
The default repository must resolve to an allowed repository. Invalid, ambiguous, blank, or mismatched configuration fails startup before stdio or HTTP begins serving. The allowlist applies to tools, aliases, discovery, resources, templates, implicit resolution, and embedded HTTP; hidden repository details are not included in selection errors. Setting only GITNEXUS_MCP_DEFAULT_REPO chooses a default without restricting explicit repository selections. An allowed repository whose name is duplicated in the registry must be configured by path, and its context resource is only served for the unique name form.
MCP response budgets
The query, context, and impact tools accept an optional positive-integer maxTokens argument. It bounds the complete formatted MCP response, including hints and error text, using a deterministic four-UTF-8-bytes-per-token estimate. When truncation is required, the response ends with … and remains valid UTF-8.
Set GITNEXUS_MCP_DEFAULT_MAX_TOKENS to apply the same guardrail when callers do not send maxTokens. An explicit tool argument takes precedence. Leaving both unset preserves the existing response byte-for-byte; this is a transport guardrail, not semantic pagination or an exact model-specific tokenizer limit.
CLI Reference
Everyday commands:
gitnexus setup # Configure MCP for detected editors (one-time; -c to select)
gitnexus analyze [path] # Index a repository (or update a stale index)
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
gitnexus serve # Start local HTTP server (multi-repo) for web UI connection
gitnexus eval-server # Start lightweight evaluation HTTP tools (loopback by default)
gitnexus list # List all indexed repositories
gitnexus status # Show index status for current repo
gitnexus clean # Delete index for current repo
gitnexus wiki [path] # Generate repository wiki from knowledge graph
gitnexus uninstall # Preview removal of GitNexus MCP/skills/hooks (--force to apply)
You can also query the graph directly from the terminal — gitnexus query, context, impact, trace, cypher, detect-changes, and check mirror the MCP tools of the same names, and gitnexus doctor prints runtime platform capabilities.
Authenticated eval-server binding
gitnexus eval-server binds to 127.0.0.1 by default. Loopback bindings do not require authentication. Any non-loopback bind, including 0.0.0.0, a LAN address, or a hostname that resolves to a LAN IPv4 address, requires GITNEXUS_AUTH_TOKEN. Every endpoint then requires an exact Authorization: Bearer <token> header.
GITNEXUS_AUTH_TOKEN='replace-me' gitnexus eval-server --host 0.0.0.0
The token may be set in the shell, .env.local, or .env in the working directory. Precedence is shell > .env.local > .env. Only GITNEXUS_AUTH_TOKEN is read from those files; their other values are not added to the process environment. Keep token files uncommitted.
All analyze flags
gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS rebuild
gitnexus analyze --repair-fts # Fast path: rebuild/verify only FTS indexes on existing index data
gitnexus analyze --skills # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
gitnexus analyze --embeddings [limit] # Enable embedding generation (slower, better search)
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-skills # Skip installing standard skill files under .claude/skills/ and .agents/skills/
gitnexus analyze --skip-git # Index folders that are not Git repositories
gitnexus analyze --default-branch develop # Branch used in the generated regression-compare example (base_ref)
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
gitnexus analyze --worker-timeout 60 # Increase worker idle timeout for slow parses
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 analyze --wal-checkpoint-threshold 67108864 # LadybugDB WAL auto-checkpoint threshold in bytes
# (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
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 --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:
gitnexus analyze --embeddings # default 50,000 node safety cap
gitnexus analyze --embeddings 0 # disable the cap entirely
gitnexus analyze --embeddings 100000 # custom cap
If embeddings are skipped on a large repository, the indexed graph likely exceeds the default cap — re-run with --embeddings 0 or a higher limit.
Repository groups (multi-repo / monorepo service tracking)
gitnexus group create <name> # Create a repository group
gitnexus group add <group> <groupPath> <registryName> # Add a repo. <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 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
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 standard skill files under .claude/skills/ and .agents/skills/
"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_AUTH_TOKEN |
unset | Bearer token required when eval-server binds beyond loopback. May also be read from .env.local or .env; shell values take precedence. |
Exposing the evaluation HTTP tools to a container, VM, or LAN. |
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. |
PROF_LBUG_LOAD |
unset | When 1, emits one [lbug-load prof] summary line per loadGraphToLbug call breaking the graph-DB persistence wall into stages (csv-emit / copy-nodes / copy-rels / fallback / total) plus node & edge counts. Zero-cost when unset. |
Attributing large-repo analyze wall time across CSV generation vs. LadybugDB COPY (issue #2203) — the analyze "emit" timing is the scope-resolution bucket, not this DB-write path. |
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_WORKER_READY_TIMEOUT_MS |
5000 |
Startup budget in milliseconds for a parse worker to load its grammar bindings and report {type:'ready'}. Slots that miss it are treated as startup crashes. |
Slow or heavily loaded hosts where a full pool cold-starting concurrently needs more than 5s, and analyze aborts with "did not report ready within 5000ms". |
GITNEXUS_FTS_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 for matching repository comments. Re-run gitnexus analyze --repair-fts after changing it. |
Keyword search quality is poor for non-English comments or identifiers under English stemming. |
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_LBUG_BUFFER_POOL_SIZE |
min(2 GiB, 80% RAM) | LadybugDB buffer-pool ceiling in bytes for every GitNexus database (analyze, MCP server, serve, group bridges). 0 restores LadybugDB's native unbounded default of 80% of system RAM; invalid values warn and fall back to the default (#2557). During analyze the pool is right-sized to the graph, scaled on non-4 KiB-page hosts 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. |
A long-lived gitnexus mcp or a big incremental analyze uses too much memory, or a huge repo's working set genuinely needs a pool larger than 2 GiB. |
GITNEXUS_LBUG_MAX_DB_SIZE |
17179869184 (16 GiB) |
Maximum size in bytes of a single LadybugDB database file — an mmap/disk-address-space ceiling, not a memory limit (it does not constrain the buffer pool). Invalid values silently fall back to the default. | Indexing a genuinely huge monorepo whose on-disk graph index approaches 16 GiB. |
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_WORKER_SHUTDOWN_DRAIN_MS |
30000 |
Max wait at pool shutdown for a retired worker still inside native code. The worker is terminated at its next JS-safe point instead of mid-native-call (which aborts the whole process with Napi::Error, #2432); on expiry it is left running, unref'd, and terminated when it surfaces. |
Shutdown latency matters more than draining a wedged worker (lower), or a legitimately-slow native grammar needs longer to surface (raise). |
GITNEXUS_CPP_CAPTURE_BUDGET_MS |
20000 |
Per-file wall-clock budget for C++ capture extraction. On breach the file keeps the captures accumulated so far and logs a warning — the worker returns to JS instead of stalling in native-heavy loops (#2432). 0 expires immediately. |
Pathological generated C++ that still exceeds the budget after the indexed lookups; raise for completeness, lower to fail-fast. |
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. |
GITNEXUS_MCP_READ_ONLY |
unset | Set to 1 to expose only proven single-repository read tools and resources; 0 disables the policy and any other value fails startup. |
The MCP server runs in an environment where graph mutation, raw Cypher, and cross-repository group routing must be unavailable. |
GITNEXUS_MCP_ALLOWED_REPOS |
unset | Comma-separated allowlist of canonical indexed repository names or absolute paths. Invalid, ambiguous, or blank entries fail startup. | One MCP process must expose only a bounded subset of the repositories in the global registry. |
GITNEXUS_MCP_DEFAULT_REPO |
unset | Canonical indexed repository name or absolute path used when a tool or resource omits its repository. Must belong to the allowlist when one is set. | Several repositories are available but unqualified MCP calls should resolve deterministically. |
GITNEXUS_MCP_DEFAULT_MAX_TOKENS |
unset | Default positive-integer response budget for MCP query, context, and impact, estimated at four UTF-8 bytes per token. Explicit maxTokens wins. |
Long MCP responses consume too much model context and callers cannot reliably add a per-request budget. |
GITNEXUS_PUBLIC_ORIGIN |
unset | The single browser origin serve is reached through, added to the CORS allowlist and to the write-route origin guard. A wildcard bind (0.0.0.0) has no host identity, so without this the server's own UI is refused. Setting it currently refuses to start: serve has no authentication, requests carrying no Origin header already reach POST /api/analyze and DELETE /api/repo, and this is the setting that would admit browser writes on top of that. Matching rules for when the gate lifts: the hostname must match exactly, and so must the scheme. A value with no scheme (app.example.com) means https, since a bare host comes from platform service discovery and those terminate TLS; spell out http://app.example.com for plain HTTP. An explicit port must match; with no port, any port on that hostname is accepted. Anything that is not one reachable host (a list, *, a bare port number, a :0 port, a trailing dot) warns at startup and allows nothing. |
gitnexus serve runs behind a reverse proxy or on a wildcard bind, and the UI's index/delete requests return origin_not_allowed. |
GITNEXUS_TRUST_PROXY |
loopback, linklocal, uniquelocal |
Express trust proxy value — which upstream hops may set X-Forwarded-*, and so what the per-IP rate limiter reads as the client IP. Set it to the exact number of proxies you control. Every hop past that is one more entry of the chain the caller gets to write. false/no/off (and a 0 hop count) trust no hop; a proxy list Express can compile (loopback, 10.0.0.0/8, 127.0.0.1) names them instead. true/yes/on is rejected: it reads the client-controlled leftmost X-Forwarded-For entry, so a spoofed chain earns a fresh rate-limit key per request, and express-rate-limit rejects it too (ERR_ERL_PERMISSIVE_TRUST_PROXY). Counts above 16 are rejected as well, as a sanity ceiling rather than a safety boundary. Any invalid value warns and falls back to the default. Bind non-loopback with this unset and serve warns: a load balancer outside the private ranges is untrusted, so every request keys to the balancer and the per-IP limit becomes one shared limit. |
serve sits behind a load balancer outside the private ranges (AWS ALB, Cloudflare, CGNAT), where every request otherwise collapses to the proxy hop and rate limiting goes global. |
gitnexus uninstall
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.
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.
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
Control flow (CFG, opt-in --pdg) — per-function control-flow graphs (BasicBlock nodes + CFG edges) feeding the PDG/taint substrate, currently TypeScript & JavaScript (#2081 M1); other languages planned. Off by default.
Multi-Repo 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.
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.
Architecture diagram
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
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
More examples: search · context · detect_changes · rename · Cypher
Process-Grouped Search
query({search_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 (default model: minimax/minimax-m2.5)
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 codebases 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)
# Allow a specific LAN/self-hosted HTTP LLM host (HTTPS is preferred for remote endpoints)
gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
# Or set a comma-separated host allowlist:
GITNEXUS_ALLOW_INSECURE_CONNECTION=llama-box.local,192.168.1.23
# Change the output language
gitnexus wiki --lang <lang> # e.g. english, chinese, spanish, japanese
For safety, http:// LLM base URLs are allowed by default only for loopback hosts (localhost, 127.0.0.1, ::1). --allow-insecure-connection and GITNEXUS_ALLOW_INSECURE_CONNECTION accept exact hostnames or IP addresses only; do not include schemes, ports, paths, credentials, or wildcards.
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.
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.
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 — it auto-detects the server and shows all your indexed repos, with full AI chat support. No re-upload, no re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
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
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.
The official setup ships two signed images, published identically to GitHub Container Registry (GHCR) and Docker Hub — same build, same digest, same Cosign signature:
| 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 |
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
Heads-up — image rename. Earlier releases published the web UI under
ghcr.io/abhigyanpatwari/gitnexus. That slug now hosts the CLI/server image and the UI moved toghcr.io/abhigyanpatwari/gitnexus-web. Previous tags remain pullable, but new versions are only published under the new slugs — update yourdocker run/ compose files (or just adopt the bundled compose).
Direct docker run & env file
# 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
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.
Versioning & supply-chain protection (Cosign signatures, provenance, Kubernetes admission policy)
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. 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.
Enterprise
GitNexus is available as an enterprise offering — fully managed SaaS or self-hosted deployment. Commercial use of the OSS version is also available 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 email founders@akonlabs.com
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) |
| KiloCode MCP workflow | @oktanishq | Guide to connect GitNexus MCP to Kilo Code and verify tools. |
Have a project built on GitNexus? Open a PR to add it here!
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
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
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 |
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.
Star History
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