Merge pull request #36 from abhigyanpatwari/gitnexus_cli

Gitnexus cli
This commit is contained in:
Abhigyan Patwari 2026-02-17 16:32:17 +05:30 committed by GitHub
commit e79133daaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
254 changed files with 38577 additions and 28804 deletions

View file

@ -0,0 +1,75 @@
{
"permissions": {
"allow": [
"WebSearch",
"WebFetch(domain:cursor.com)",
"WebFetch(domain:composio.dev)",
"Bash(npx tsc:*)",
"Bash(claude rename:*)",
"Bash(npm run build:*)",
"Bash(npm link:*)",
"Bash(gitnexus --version:*)",
"Bash(gitnexus --help:*)",
"Bash(npm ls:*)",
"Bash(gitnexus augment:*)",
"Bash(node -e \"\nconst { augment } = await import\\(''./gitnexus/dist/core/augmentation/engine.js''\\);\ntry {\n const r = await augment\\(''setup'', process.cwd\\(\\)\\);\n console.log\\(''Result:'', r ? r.substring\\(0, 200\\) : ''null''\\);\n} catch\\(e\\) { console.error\\(''Error:'', e.message\\); }\nprocess.exit\\(0\\);\n\")",
"Bash(cmd.exe /c \"cd /d D:\\\\Projects\\\\GitnexusV2 && gitnexus augment setup\")",
"Bash(cmd.exe /c \"cd /d D:\\\\Projects\\\\GitnexusV2 && gitnexus status\")",
"Bash(gh repo clone:*)",
"Bash(claude mcp:*)",
"Bash(gh issue view:*)",
"Bash(echo:*)",
"Bash(node:*)",
"Bash(npm view:*)",
"Bash(npm version:*)",
"Bash(npm pack:*)",
"Bash(npm publish:*)",
"Bash(npx gitnexus:*)",
"mcp__gitnexus__list_repos",
"mcp__gitnexus__query",
"mcp__gitnexus__context",
"mcp__gitnexus__impact",
"Bash(git add:*)",
"Bash(Glob)",
"Bash(Bash\"\\) per new Claude Code schema\n- Rename gitnexus-hook.js → gitnexus-hook.cjs for CommonJS compatibility\n- Fix setup.ts: correct hook filename and timeout \\(8000ms instead of 10ms\\)\n- Bump to v1.1.9 and publish to npm\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git push:*)",
"WebFetch(domain:docs.kuzudb.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:read.engineerscodex.com)",
"WebFetch(domain:towardsdatascience.com)",
"WebFetch(domain:kilo.ai)",
"WebFetch(domain:deepwiki.com)",
"WebFetch(domain:turbopuffer.com)",
"WebFetch(domain:windsurf.com)",
"WebFetch(domain:modal.com)",
"WebFetch(domain:www.augmentcode.com)",
"WebFetch(domain:www.qodo.ai)",
"WebFetch(domain:arxiv.org)",
"WebFetch(domain:cognition.ai)",
"WebFetch(domain:microsoft.github.io)",
"WebFetch(domain:github.github.com)",
"WebFetch(domain:gist.github.com)",
"WebFetch(domain:fsoft-ai4code.github.io)",
"mcp__gitnexus__cypher",
"WebFetch(domain:repomix.com)",
"WebFetch(domain:www.humanlayer.dev)",
"WebFetch(domain:agents.md)",
"WebFetch(domain:eclipsesource.com)",
"WebFetch(domain:www.usefulfunctions.co.uk)",
"WebFetch(domain:developers.googleblog.com)",
"WebFetch(domain:www.anthropic.com)",
"WebFetch(domain:www.driver.ai)",
"WebFetch(domain:blog.sshh.io)",
"WebFetch(domain:docs.qodo.ai)",
"WebFetch(domain:smartlogic.io)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(grep:*)"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"gitnexus"
]
}

View file

@ -0,0 +1,85 @@
---
name: gitnexus-debugging
description: Trace bugs through call chains using knowledge graph
---
# Debugging with GitNexus
## When to Use
- "Why is this function failing?"
- "Trace where this error comes from"
- "Who calls this method?"
- "This endpoint returns 500"
- Investigating bugs, errors, or unexpected behavior
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
## Debugging Patterns
| Symptom | GitNexus Approach |
|---------|-------------------|
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
| Recent regression | `detect_changes` to see what your changes affect |
## Tools
**gitnexus_query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
```
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
→ Step 3: validatePayment → calls fetchRates (external)
4. Root cause: fetchRates calls external API without proper timeout
```

View file

@ -0,0 +1,75 @@
---
name: gitnexus-exploring
description: Navigate unfamiliar code using GitNexus knowledge graph
---
# Exploring Codebases with GitNexus
## When to Use
- "How does authentication work?"
- "What's the project structure?"
- "Show me the main components"
- "Where is the database logic?"
- Understanding code you haven't seen before
## Workflow
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
## Resources
| Resource | What you get |
|----------|-------------|
| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) |
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) |
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) |
## Tools
**gitnexus_query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
```
## Example: "How does payment processing work?"
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
```

View file

@ -0,0 +1,94 @@
---
name: gitnexus-impact-analysis
description: Analyze blast radius before making code changes
---
# Impact Analysis with GitNexus
## When to Use
- "Is it safe to change this function?"
- "What will break if I modify X?"
- "Show me the blast radius"
- "Who uses this code?"
- Before making non-trivial code changes
- Before committing — to understand what your changes affect
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
## Understanding Output
| Depth | Risk Level | Meaning |
|-------|-----------|---------|
| d=1 | **WILL BREAK** | Direct callers/importers |
| d=2 | LIKELY AFFECTED | Indirect dependencies |
| d=3 | MAY NEED TESTING | Transitive effects |
## Risk Assessment
| Affected | Risk |
|----------|------|
| <5 symbols, few processes | LOW |
| 5-15 symbols, 2-5 processes | MEDIUM |
| >15 symbols or many processes | HIGH |
| Critical path (auth, payments) | CRITICAL |
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
maxDepth: 3
})
→ d=1 (WILL BREAK):
- loginHandler (src/auth/login.ts:42) [CALLS, 100%]
- apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]
→ d=2 (LIKELY AFFECTED):
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
→ Risk: MEDIUM
```
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
2. READ gitnexus://repo/my-app/processes
→ LoginFlow and TokenRefresh touch validateUser
3. Risk: 2 direct callers, 2 processes = MEDIUM
```

View file

@ -0,0 +1,113 @@
---
name: gitnexus-refactoring
description: Plan safe refactors using blast radius and dependency mapping
---
# Refactoring with GitNexus
## When to Use
- "Rename this function safely"
- "Extract this into a module"
- "Split this service"
- "Move this to a new file"
- Any task involving renaming, extracting, splitting, or restructuring code
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
```
## Risk Rules
| Risk Factor | Mitigation |
|-------------|------------|
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

@ -0,0 +1 @@
Subproject commit e90622aa24c92d7a08712085d2355b9fe36cf3c3

@ -0,0 +1 @@
Subproject commit e90622aa24c92d7a08712085d2355b9fe36cf3c3

@ -0,0 +1 @@
Subproject commit 44572ad0bddea03d548b5f25ace720ba4f4c1548

View file

@ -1,239 +0,0 @@
---
name: Enhance
overview: Restructure GitNexus LLM tools to leverage clusters and processes for better code understanding. Remove unused highlight tool, add new tools (explore, overview), enhance existing tools with cluster/process context, and improve impact analysis reliability.
todos: []
---
# Enhanced LLM Tools with Cluster and Process Integration
## Summary
Consolidate GitNexus from 6 tools to **7 focused tools** that leverage the pre-computed clusters (Communities) and processes for richer context. Remove the highlight tool, add `explore` and `overview` tools, and enhance `search` and `blastRadius` with cluster/process awareness.
## Final Tool Set
| Tool | Status | Purpose ||------|--------|---------|| `search` | Enhance | Hybrid search + group results by process/cluster || `grep` | Keep | Regex pattern search || `read` | Keep | Read file content || `explore` | **New** | Deep dive on one symbol, cluster, or process || `overview` | **New** | Codebase map (all clusters + all processes) || `impact` | Enhance | Rename from blastRadius, add process/cluster context, increase limits || `cypher` | Keep | Raw graph queries || `highlight` | **Remove** | No longer needed |
## Architecture
```mermaid
flowchart TD
subgraph tools [LLM Tools Layer]
search[search]
grep[grep]
read[read]
explore[explore]
overview[overview]
impact[impact]
cypher[cypher]
end
subgraph graph [Knowledge Graph]
nodes[Nodes: File, Function, Class...]
communities[Community Nodes]
processes[Process Nodes]
edges[CodeRelation Edges]
memberOf[MEMBER_OF Edges]
stepIn[STEP_IN_PROCESS Edges]
end
search --> edges
search --> communities
search --> processes
explore --> communities
explore --> processes
explore --> memberOf
explore --> stepIn
overview --> communities
overview --> processes
impact --> edges
impact --> communities
impact --> processes
cypher --> graph
```
## File Changes
### 1. Remove Highlight Tool
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)
- Delete the `highlightTool` definition (lines ~395-414)
- Remove `highlightTool` from the returned array (line ~862)
- Remove highlight marker logic from `blastRadius` output (line ~814-816)
**File:** [gitnexus/src/core/llm/agent.ts](gitnexus/src/core/llm/agent.ts)
- Remove highlight references from system prompt (lines 70, 77)
- Update tool list in prompt to reflect new tools
**File:** [gitnexus/src/core/llm/types.ts](gitnexus/src/core/llm/types.ts)
- Remove `'highlight'` from `AgentStreamChunk.type` union (line 180)
- Remove `highlightNodeIds` property (line 187-188)
### 2. Add `explore` Tool
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)New tool that auto-detects target type and returns comprehensive context:
```typescript
explore({
target: string, // Name of symbol, cluster, or process
type?: 'symbol' | 'cluster' | 'process' // Optional, auto-detected
})
```
**Functionality:**
- For symbols: Query node, get MEMBER_OF cluster, get STEP_IN_PROCESS processes, get 1-hop connections
- For clusters: Query Community node, get members via MEMBER_OF, get processes that touch this cluster
- For processes: Query Process node, get steps via STEP_IN_PROCESS with step order, get clusters touched
**Cypher queries needed:**
```cypher
-- Symbol cluster membership
MATCH (s {name: $name})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
RETURN c.label, c.description
-- Symbol process participation
MATCH (s {name: $name})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
RETURN p.label, r.step, p.stepCount
-- Process steps in order
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: $processId})
RETURN s.name, s.filePath, r.step
ORDER BY r.step
```
### 3. Add `overview` Tool
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)New tool that returns codebase structure:
```typescript
overview() // No parameters
```
**Functionality:**
- Query all Community nodes with member counts
- Query all Process nodes with step counts and types
- Calculate cluster dependencies (cross-cluster CALLS)
- Identify critical paths (most connected processes)
**Output format:**
```javascript
CLUSTERS (N total):
| Cluster | Symbols | Cohesion | Description |
...
PROCESSES (N total):
| Process | Steps | Type | Clusters |
...
CRITICAL PATHS:
- LoginFlow (45 edges)
...
```
### 4. Enhance `search` Tool
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)Modify existing search to group results by process:**Current:** Returns flat list with 1-hop connections**Enhanced:** Groups results by process, adds cluster context**Changes:**
- After hybrid search, query STEP_IN_PROCESS for each result
- Group results by process ID
- Sort processes by number of matching results (relevance)
- Add cluster label for each result via MEMBER_OF query
- Keep 1-hop connections as optional detail
**New parameter:**
```typescript
search({
query: string,
groupByProcess?: boolean, // Default: true
limit?: number
})
```
### 5. Enhance `impact` Tool (rename from blastRadius)
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)**Rename:** `blastRadiusTool` to `impactTool`**Enhancements:**
1. Increase LIMIT clauses: 100 to 300 (depth 1), 100 to 200 (depth 2), 50 to 100 (depth 3)
2. Add affected processes section (query STEP_IN_PROCESS for all affected symbols)
3. Add affected clusters section (query MEMBER_OF for all affected symbols)
4. Add risk assessment summary
5. Surface confidence scores more prominently (group by confidence level)
**New output sections:**
```javascript
AFFECTED PROCESSES:
- LoginFlow - BROKEN at step 2
- SignupFlow - BROKEN at step 1
AFFECTED CLUSTERS:
- Authentication (direct)
- API Routes (indirect)
RISK: CRITICAL
- N direct callers
- N processes affected
- N clusters affected
```
### 6. Increase Process Detection Limits
**File:** [gitnexus/src/core/ingestion/process-processor.ts](gitnexus/src/core/ingestion/process-processor.ts)Change default config (lines 27-32):
```typescript
const DEFAULT_CONFIG: ProcessDetectionConfig = {
maxTraceDepth: 10, // Keep
maxBranching: 4, // Was 3
maxProcesses: 75, // Was 50
minSteps: 2, // Keep
};
```
### 7. Update System Prompt
**File:** [gitnexus/src/core/llm/agent.ts](gitnexus/src/core/llm/agent.ts)Update BASE_SYSTEM_PROMPT to reflect new tools:
```javascript
## TOOLS
- **search** - Hybrid search. Results grouped by process with cluster context.
- **grep** - Regex pattern search for exact strings.
- **read** - Read file content.
- **explore** - Deep dive on a symbol, cluster, or process. Shows membership, participation, connections.
- **overview** - Codebase map showing all clusters and processes.
- **impact** - Impact analysis. Shows affected processes, clusters, and risk level.
- **cypher** - Raw Cypher queries against the graph.
## GRAPH SCHEMA
Nodes: File, Folder, Function, Class, Interface, Method, Community, Process
Relations: CodeRelation with type: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
```
## Implementation Order
1. Remove highlight tool (cleanup)
2. Increase process detection limits
3. Add overview tool (simplest new tool)
4. Add explore tool
5. Enhance impact tool

5
.cursorrules Normal file
View file

@ -0,0 +1,5 @@
# AI Agent Rules
Follow .gitnexus/RULES.md for all project context and coding guidelines.
This project uses GitNexus MCP for code intelligence. See .gitnexus/RULES.md for available tools and best practices.

7
.gitignore vendored
View file

@ -40,3 +40,10 @@ coverage/
.env*.local
.gitnexus
# Assets (screenshots, images)
assets/
# Generated files (should not be indexed)
repomix-output*

9
.mcp.json Normal file
View file

@ -0,0 +1,9 @@
{
"mcpServers": {
"gitnexus": {
"type": "stdio",
"command": "cmd",
"args": ["/c", "npx", "-y", "gitnexus@latest", "mcp"]
}
}
}

5
.windsurfrules Normal file
View file

@ -0,0 +1,5 @@
# AI Agent Rules
Follow .gitnexus/RULES.md for all project context and coding guidelines.
This project uses GitNexus MCP for code intelligence. See .gitnexus/RULES.md for available tools and best practices.

64
AGENTS.md Normal file
View file

@ -0,0 +1,64 @@
# AI Agent Rules
<!-- gitnexus:start -->
# GitNexus MCP
This project is indexed by GitNexus as **GitnexusV2** (1295 symbols, 3262 relationships, 99 execution flows).
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
## Always Start Here
For any task involving code understanding, debugging, impact analysis, or refactoring, you must:
1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
## Skills
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/refactoring/SKILL.md` |
## Tools Reference
| Tool | What it gives you |
|------|-------------------|
| `query` | Process-grouped code intelligence — execution flows related to a concept |
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
| `detect_changes` | Git-diff impact — what do your current changes affect |
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
| `list_repos` | Discover indexed repos |
## Resources Reference
Lightweight reads (~100-500 tokens) for navigation:
| Resource | Content |
|----------|---------|
| `gitnexus://repo/{name}/context` | Stats, staleness check |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
| `gitnexus://repo/{name}/processes` | All execution flows |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |
## Graph Schema
**Nodes:** File, Function, Class, Interface, Method, Community, Process
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
RETURN caller.name, caller.filePath
```
<!-- gitnexus:end -->

View file

@ -1,585 +1,247 @@
# PyBaMM Architecture - End-to-End Analysis
# GitNexus Architecture
## Executive Summary
> Auto-generated from the GitNexus knowledge graph (1021 symbols, 2552 edges, 79 execution flows).
**PyBaMM** (Python Battery Mathematical Modelling) is a comprehensive, open-source framework for modeling and simulating battery behavior. The project contains **973 files**, **4,342 functions**, and **735 classes** organized in a layered architecture optimized for modularity, extensibility, and scientific computation.
## Overview
---
GitNexus is a graph-powered code intelligence platform that indexes codebases into a knowledge graph and exposes them via MCP (Model Context Protocol) tools for AI agents. It consists of three packages:
## 🏗️ High-Level Architecture Layers
- **`gitnexus/`** — Core CLI + MCP server (published to npm). Indexes repositories, stores graphs in KuzuDB, and serves queries.
- **`gitnexus-web/`** — Browser-based frontend with WebAssembly tree-sitter and in-browser KuzuDB.
- **`gitnexus-claude-plugin/`** / **`gitnexus-cursor-integration/`** — IDE integrations that augment AI agent tool calls with graph context.
## Functional Areas
The codebase is organized into 14 functional clusters detected by community analysis:
| Module | Symbols | Cohesion | Responsibility |
|--------|---------|----------|----------------|
| **Ingestion** | 108 | 24% | Multi-phase pipeline: file walking, tree-sitter parsing, import resolution, call tracing, heritage extraction, community detection, process tracing |
| **Kuzu** | 50 | 23% | KuzuDB graph storage adapter, CSV generation, schema management, query execution |
| **Embeddings** | 46 | 35% | Embedding pipeline: text generation from symbols, ONNX model inference, vector storage |
| **Components** | 41 | 35% | Web UI React components (graph visualization, search, navigation) |
| **Local** | 38 | 15% | MCP backend: tool implementations (query, context, impact, rename), resource handlers, search (BM25 + semantic) |
| **Workers** | 38 | 27% | Web Workers for browser-side ingestion and tree-sitter parsing; Node.js worker threads for parallel parsing |
| **LLM** | 28 | 37% | LLM-based cluster enrichment, prompt building, provider abstraction |
| **CLI** | 24 | 35% | Command handlers (analyze, setup, serve, mcp), AI context file generation, IDE hook/skill installation |
| **Storage** | 22 | 32% | Repository registry, `.gitnexus/` directory management, staleness detection |
| **Services** | 14 | 31% | Shared services (config, ignore patterns, language support) |
| **Hooks** | 12 | 56% | Claude Code / Cursor hook scripts for augmenting search tools with graph context |
| **Search** | 11 | 36% | Hybrid search: BM25 keyword index + semantic vector search with reciprocal rank fusion |
## Key Execution Flows
### 1. CLI Analyze Pipeline
The primary ingestion path when a user runs `npx gitnexus analyze`:
```
┌─────────────────────────────────────────────────────────────┐
│ USER INTERFACE & EXAMPLES │
│ (Jupyter Notebooks, Scripts, Experiments) │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ SIMULATION & EXPERIMENT ORCHESTRATION LAYER │
│ • Simulation - High-level simulation runner │
│ • Experiment - Define charging/discharging cycles │
│ • BatchStudy - Multi-parameter studies │
│ • Callbacks - Monitor simulation progress │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ MODEL LAYER (Hierarchical) │
├─ BaseBatteryModel (Physical domain constraints) │
├─ Full Models: │
│ ├─ Lithium-Ion (DFN, SPM, SPMe, MPM, MSMR, etc.) │
│ ├─ Lead-Acid (Full, LOQS models) │
│ ├─ Sodium-Ion (emerging battery chemistry) │
│ └─ Equivalent Circuit Models (ECM) │
├─ Submodels (Pluggable domain-specific components): │
│ ├─ Particle Diffusion (kinetics in electrodes) │
│ ├─ Electrode Kinetics (Butler-Volmer, Marcus, etc.) │
│ ├─ Interface Chemistry (SEI growth, Li-plating, OCP) │
│ ├─ Thermal Management (lumped, distributed 1D-3D) │
│ ├─ Current Collector Physics │
│ ├─ Electrolyte Transport (conductivity, diffusion) │
│ ├─ Convection (internal circulation) │
│ ├─ Porosity & Tortuosity (pore network) │
│ └─ Active Material Loss (cycling degradation) │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ EXPRESSION TREE (Symbolic Computation Layer) │
│ Directed Acyclic Graph (DAG) of mathematical expressions │
├─ Symbol - Base class for all nodes │
│ ├─ Variable - State vector entries │
│ ├─ Parameter - Model parameters │
│ ├─ Scalar/Array - Constants │
│ ├─ StateVector - Discretized spatial domain │
│ └─ InputParameter - Time-varying inputs │
├─ Operators │
│ ├─ BinaryOperators - +, -, *, /, power, etc. │
│ ├─ UnaryOperators - exp, log, sin, cos, etc. │
│ ├─ Concatenations - Stack vectors │
│ └─ Broadcasts - Repeat/tile operations │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ DISCRETISATION LAYER (PDE → ODE/DAE Conversion) │
│ Transforms continuous PDEs into discrete systems │
├─ Discretisation - Master converter class │
├─ Spatial Methods: │
│ ├─ FiniteVolume - 1D/2D finite volume schemes │
│ ├─ SpectralVolume - Spectral approach │
│ ├─ ScikitFiniteElement - 1D unstructured meshes │
│ ├─ ScikitFiniteElement3D- 3D tetrahedral meshes │
│ └─ ZeroDimensionalMethod- Lumped (0D) approximations │
├─ Meshes: │
│ ├─ 1D Submeshes - Line domains │
│ ├─ 2D Submeshes - Sheet domains │
│ ├─ 3D Submeshes - Volume domains (via scikit-fem)│
│ └─ Composite Meshes - Combined domains │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ SOLVER LAYER (DAE System Integration) │
│ Converts discrete system → numerical solution │
├─ Solver Interfaces: │
│ ├─ BaseSolver - Abstract interface │
│ ├─ ODE Solvers: │
│ │ ├─ ScipySolver - scipy.integrate.ode │
│ │ ├─ JAXSolver - JAX backend (jit-compiled) │
│ │ ├─ JAXBDFSolver - JAX BDF method │
│ │ └─ IDAKLUSolver - SUNDIALS IDA (C++ wrapper) │
│ ├─ DAE Solvers: │
│ │ ├─ CasadiSolver - CasADi symbolic optimization │
│ │ ├─ IDakluJax - IDA + JAX hybrid │
│ │ └─ AlgebraicSolver - Solve algebraic eqns only │
│ └─ Special: │
│ ├─ DummySolver - Testing/debugging │
│ └─ Solution - Stores results + post-process │
├─ Features: │
│ ├─ Jacobian Computation - Auto diff or symbolic │
│ ├─ Event Detection - Trigger on state changes │
│ ├─ Callbacks - Hooks during integration │
│ └─ Processed Variables - Post-compute derived quantities│
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ PARAMETER & DATA LAYER │
│ Manages model coefficients and experimental data │
├─ ParameterValues - Substitutes symbols → numbers │
├─ Parameter Sets: │
│ ├─ Lithium-Ion Parameter Sets (Chen2020, OKane2022, etc)│
│ ├─ Lead-Acid Parameter Sets (Sulzer2019) │
│ ├─ Sodium-Ion Parameter Sets (Chayambuka2022) │
│ └─ ECM Parameter Sets (voltage model coefficients) │
├─ Special Parameters: │
│ ├─ ElectricalParameters - Conductivity, diffusivity │
│ ├─ ThermalParameters - Heat capacity, conductivity │
│ ├─ GeometricParameters - Dimensions, areas, volumes │
│ └─ ProcessParameterData - Fit to experimental results │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ VISUALIZATION & POST-PROCESSING │
│ Analysis and interpretation of results │
├─ Plotting Modules: │
│ ├─ quick_plot() - 1-line quick visualization │
│ ├─ plot() - Customizable plotting │
│ ├─ plot_voltage_components() - Decompose voltage │
│ ├─ plot_summary_variables() - Key metrics │
│ ├─ plot_3d_heatmap() - 3D temperature fields │
│ └─ plot_3d_cross_section() - 2D slices of 3D │
├─ Dynamic Plotting: │
│ └─ DynamicPlot - Live update during solving │
└──────────────────────────────────────────────────────────────┘
analyzeCommand (cli/analyze.ts)
→ runPipelineFromRepo (ingestion/pipeline.ts)
→ walkRepository — concurrent file I/O (32 parallel reads)
→ processStructure — folder/file graph nodes
→ processParsing — tree-sitter AST parsing (worker threads)
→ processImports — import/require/use resolution
→ processCalls — function call tracing with confidence
→ processHeritage — extends/implements relationships
→ processCommunities — Leiden community detection
→ processProcesses — execution flow tracing
→ loadGraphToKuzu — CSV export → KuzuDB bulk import
→ runEmbeddingPipeline — generate + store symbol embeddings
→ generateAIContextFiles — write CLAUDE.md, AGENTS.md, skills
```
---
### 2. MCP Server Request Flow
## 📊 Core Components Deep Dive
### 1. **Expression Tree (Symbolic Computation)**
**Purpose:** Represents mathematical expressions as a directed acyclic graph (DAG).
**Key Classes:**
When an AI agent calls a GitNexus MCP tool:
```
Symbol (Base Class)
├── Variable - Represents y(t), y_dot(t)
├── Parameter - Fixed model coefficients
├── Scalar/Array - Numerical constants
├── StateVector - Discretized spatial variables
├── InputParameter - Time-varying inputs (current, temperature)
BinaryOperator
├── Addition/Subtraction
├── Multiplication/Division
├── Power
├── MatrixMultiplication
└── Equality (for algebraic equations)
UnaryOperator
├── Exponential, Logarithm
├── Trigonometric (sin, cos, tan)
├── Sign, Absolute Value
└── Specialized (exp, log, cosh, etc.)
mcpCommand (cli/mcp.ts)
→ startMCPServer (mcp/server.ts)
→ callTool (mcp/local/local-backend.ts)
→ ensureInitialized — lazy KuzuDB connection + embedder init
→ query/context/impact — graph traversal via KuzuDB Cypher
→ semanticSearch — ONNX embedding + vector similarity
→ readResource (mcp/resources.ts)
→ queryClusters/queryProcesses — direct graph queries
```
**Why This Matters:**
- Enables **symbolic differentiation** (Jacobian computation)
- **Backend-agnostic**: Same expression can be evaluated as Python, CasADi, or JAX code
- Supports **automatic code generation** for performance
---
### 2. **Model Hierarchy**
**Top Level: `BaseModel`**
- Holds empty RHS and algebraic equation dictionaries
- Manages variables, parameters, boundary conditions
- Coordinates discretisation and conversion
**Next Level: `BaseBatteryModel`**
- Enforces battery-specific physics constraints
- Implements standard lifecycle: `build_model()``discretise()``solve()`
**Bottom Level: Concrete Models (Plug-and-Play Architecture)**
| Model | Type | Complexity | Use Case |
|-------|------|-----------|----------|
| **SPM** | Lithium-Ion | Simplest | Quick simulations, education |
| **SPMe** | Lithium-Ion | Medium | Semi-empirical electrolyte |
| **DFN** | Lithium-Ion | Complex | High accuracy, research |
| **MSMR** | Lithium-Ion | Very Complex | Multi-scale particle size dist. |
| **MPM** | Lithium-Ion | Complex | Mesoscale particle modeling |
| **Half-Cell** | Lithium-Ion | Custom | Single electrode testing |
| **Thermal Models** | Any | Adds complexity | Temperature effects |
| **ECM (Thevenin)** | Equivalent Circuit | Simple | Real-time estimation |
**Submodel Pattern:**
```
Full Models = Combination of pluggable submodels
Example: DFN Model
├── Active Material (constant or loss)
├── Particle Diffusion (negative & positive electrodes)
├── Electrode Kinetics (interface reactions)
├── Open Circuit Potential (voltage lookup)
├── SEI Growth (lithium loss)
├── Current Collector (ohmic drop)
├── Convection (internal flow)
├── Thermal (heat generation & transfer)
└── External Circuit (boundary conditions)
```
---
### 3. **Discretisation Pipeline**
**Convert PDEs → Finite-Dimensional ODEs/DAEs**
### 3. Graph Storage Pipeline
```
Physics-Based PDE
[Spatial Method Selected: Finite Volume / Spectral / FEM]
Mesh Generation (1D/2D/3D depending on model)
Gradient/Divergence Operators Discretized
Boundary Conditions Applied
Expression Tree Converted (y → discretized vector)
Final System: M*dy/dt = f(t,y) + g(t,y) = 0 [DAE form]
loadGraphToKuzu (kuzu/kuzu-adapter.ts)
→ generateAllCSVs (kuzu/csv-generator.ts)
→ generateFileCSV — node CSVs per label type
→ escapeCSVField — UTF-8 sanitization
→ KuzuDB COPY FROM — bulk CSV import
→ createIndexes — property indexes for query performance
```
**Mesh Strategy:**
- **1D**: Uniform or non-uniform grids (electrodes, separator)
- **2D**: Cartesian or polar (pouch cell cross-sections)
- **3D**: Tetrahedral (scikit-fem), complex geometries
---
### 4. **Solver Pipeline**
**Goal:** Integrate DAE system over time
**Solver Family:**
- **ScipySolver**: Reliable, well-tested, pure Python
- **CasadiSolver**: Symbolic optimization, slow but accurate
- **IDAKLUSolver**: C++ SUNDIALS, fastest
- **JAXSolver**: JIT-compiled, GPU-capable
- **IDakluJax**: Hybrid IDA + JAX
**Key Features:**
- **Event Detection**: Stop when voltage hits limit
- **Jacobian**: Computed symbolically or via auto-diff
- **Callbacks**: Monitor state during integration
- **Mass Matrix**: Handle DAE systems with singular mass matrices
---
### 5. **Parameter System**
**Strategy:** Keep symbolic model separate from numerical values
### 4. Embedding Pipeline
```
Model Construction:
pybamm.Parameter("Conductivity") → generic symbol
[Stored in expression tree]
Before Solving:
parameter_values = pybamm.ParameterValues({
"Conductivity": 1.23 # Numerical value
})
parameter_values.process_model(model)
All symbols substituted with values
Ready to solve!
runEmbeddingPipeline (embeddings/embedding-pipeline.ts)
→ generateBatchEmbeddingTexts (embeddings/text-generator.ts)
→ generateEmbeddingText — symbol → natural language description
→ generateFunctionText — include signature, calls, file context
→ cleanContent — strip noise, truncate
→ embedBatch — ONNX Runtime inference (all-MiniLM-L6-v2)
→ storeEmbeddings — KuzuDB vector storage
```
**Pre-built Parameter Sets:**
- **Lithium-Ion**: Chen2020, OKane2022, Ai2020, Ecker2015, ORegan2022
- **Lead-Acid**: Sulzer2019
- **Sodium-Ion**: Chayambuka2022
- **ECM**: Thevenin model coefficients
---
## 🔄 Execution Flow: From Model to Solution
### Example: Simple SPM Simulation
```python
import pybamm
# Step 1: Create model
model = pybamm.lithium_ion.SPM()
# Step 2: Define simulation
sim = pybamm.Simulation(
model,
parameter_values=pybamm.ParameterValues("Chen2020"),
solver=pybamm.IDAKLUSolver()
)
# Step 3: Run
sim.solve([0, 3600]) # Solve 1 hour
# Step 4: Plot
sim.plot()
```
**Behind the Scenes:**
1. **Model Initialization** → Submodels concatenated
2. **Build Phase** → RHS, algebraic equations assembled
3. **Parameter Substitution** → Symbols replaced with values
4. **Discretisation** → Spatial PDE → ODE/DAE
5. **Jacobian Computation** → Auto-differentiation
6. **Solver Setup** → Initial conditions, events configured
7. **Integration Loop** → Time-stepping with callbacks
8. **Post-Processing** → Compute derived variables (impedance, etc.)
9. **Visualization** → Plot results
---
## 🔗 Key Dependencies & Data Flow
### Upstream (Inputs)
```
Experiment (current profile)
ParameterValues (physical constants)
Geometry (cell dimensions)
ModelOptions (choose submodels)
BaseModel
```
### Downstream (Outputs)
```
Discretisation
DAE System (M*dy/dt = f(t,y))
Solver
Solution object (t, y, processed_variables)
Plotting/Analysis
Results (voltage, capacity, temperature, etc.)
```
---
## 🌳 Hotspot Nodes (Most Connected Components)
These are the "hubs" that everything depends on:
| Node | Type | Connections | Role |
|------|------|-----------|------|
| `src/pybamm/__init__.py` | File | **500** | Central export hub |
| `Variable` | Class | **474** | Core state representation |
| `Scalar` | Class | **397** | Constant handling |
| `evaluate()` | Function | **344** | Expression evaluation |
| `solve()` | Function | **311** | Solver invocation |
| `BaseModel` | Class | **305** | Model parent |
| `Discretisation` | Class | **289** | Discretisation orchestration |
| `linspace()` | Function | **267** | Mesh generation |
---
## 📁 Directory Structure
### 5. Web App Pipeline (Browser)
```
src/pybamm/
├── models/ # Model hierarchy
│ ├── base_model.py # Abstract base
│ ├── full_battery_models/ # Concrete implementations
│ │ ├── lithium_ion/
│ │ ├── lead_acid/
│ │ ├── sodium_ion/
│ │ └── equivalent_circuit/
│ └── submodels/ # Pluggable physics components
│ ├── interface/ # Electrode kinetics, SEI, OCP
│ ├── particle/ # Particle diffusion
│ ├── thermal/ # Heat transfer
│ ├── electrode/ # Ohmic drop
│ ├── convection/ # Internal flow
│ └── [more...]
├── expression_tree/ # Symbolic DAG
│ ├── symbol.py # Base class
│ ├── binary_operators.py # +, -, *, /
│ ├── unary_operators.py # sin, exp, log
│ ├── operations/ # Evaluation, Jacobian, serialization
│ └── [more...]
├── discretisations/ # PDE → ODE conversion
│ └── discretisation.py
├── spatial_methods/ # Finite volume, spectral, FEM
│ ├── finite_volume.py
│ ├── spectral_volume.py
│ └── [more...]
├── meshes/ # Grid generation
│ ├── meshes.py
│ └── [submesh types...]
├── solvers/ # DAE integration
│ ├── base_solver.py
│ ├── scipy_solver.py
│ ├── casadi_solver.py
│ ├── idaklu_solver.py
│ └── [more...]
├── parameters/ # Physical coefficients
│ ├── base_parameters.py
│ ├── parameter_values.py
│ ├── lithium_ion_parameters.py
│ └── input/
│ └── parameters/ # Pre-built parameter sets
├── plotting/ # Visualization
│ ├── plot.py
│ ├── quick_plot.py
│ ├── plot_voltage_components.py
│ └── [more...]
├── batch_study.py # Multi-parameter studies
├── simulation.py # High-level runner
├── experiment/ # Charge/discharge cycles
└── [more...]
tests/
├── unit/ # Isolated component tests
└── integration/ # End-to-end tests
AppStateProvider (hooks/useAppState.tsx)
→ runPipeline (workers/ingestion.worker.ts) — Web Worker
→ runPipelineFromFiles (ingestion/pipeline.ts)
→ createKnowledgeGraph
→ processParsing — WASM tree-sitter
→ processImports/Calls/Heritage
→ processCommunities/Processes
→ loadGraphToKuzu — in-browser KuzuDB (WASM)
```
---
## Architecture Diagram
## 🎯 Design Patterns
```mermaid
graph TB
subgraph CLI["CLI Layer"]
analyze["analyze command"]
setup["setup command"]
mcp_cmd["mcp command"]
serve["serve command"]
augment["augment command"]
end
### 1. **Plugin Architecture (Submodels)**
- Models are built by combining plug-and-play submodels
- Easy to swap implementations (e.g., different kinetics models)
- **Example**: Switch from Butler-Volmer to Marcus kinetics
subgraph Ingestion["Ingestion Pipeline"]
walker["Filesystem Walker<br/>(concurrent I/O)"]
structure["Structure Processor"]
parsing["Parsing Processor<br/>(worker threads)"]
imports["Import Processor"]
calls["Call Processor"]
heritage["Heritage Processor"]
communities["Community Detection<br/>(Leiden)"]
processes["Process Tracing"]
end
### 2. **Expression Tree Pattern**
- Decouple symbolic math from backend
- Same expression → Python, CasADi, or JAX code
- Enables automatic differentiation
subgraph TreeSitter["Tree-Sitter"]
parser_loader["Parser Loader"]
ts_queries["Language Queries<br/>(9 languages)"]
worker_pool["Worker Pool"]
parse_worker["Parse Workers"]
end
### 3. **Factory Pattern (Solvers)**
- `solve()` returns appropriate solver based on model type
- User doesn't need to know solver implementation details
subgraph MCP["MCP Server"]
server["MCP Server<br/>(stdio transport)"]
tools["Tools: query, context,<br/>impact, rename, cypher"]
resources["Resources: clusters,<br/>processes, schema"]
backend["Local Backend"]
end
### 4. **Strategy Pattern (Spatial Methods)**
- Choose discretization strategy (FV, Spectral, FEM) at runtime
- Swap without changing model code
subgraph Storage["Storage Layer"]
kuzu["KuzuDB<br/>(graph store)"]
csv_gen["CSV Generator"]
repo_mgr["Repo Manager<br/>(~/.gitnexus registry)"]
end
### 5. **Template Method (Model Lifecycle)**
1. `model.build_model()`
2. `disc.discretise(model)`
3. `solver.solve(t_eval, y0)`
subgraph Search["Search Engine"]
bm25["BM25 Keyword Index"]
semantic["Semantic Search<br/>(all-MiniLM-L6-v2)"]
embedder["ONNX Embedder"]
end
---
subgraph Hooks["IDE Integration"]
claude_hook["Claude Code Hooks"]
cursor_hook["Cursor Hooks"]
skills["Skills<br/>(exploring, debugging,<br/>impact, refactoring)"]
end
## 🚀 Performance Considerations
subgraph Web["Web Frontend"]
app["React App"]
web_worker["Web Worker<br/>(WASM ingestion)"]
components["Graph Visualization"]
end
### Bottlenecks
1. **Discretisation**: Large spatial grids → huge state vectors
2. **Jacobian Computation**: Dense matrices for implicit solvers
3. **Parameter Substitution**: Re-expression tree traversal
%% CLI → Ingestion
analyze --> walker
walker --> structure --> parsing --> imports --> calls --> heritage --> communities --> processes
### Optimizations
1. **CasADi Backend**: Symbolic optimization + JIT
2. **JAX Solver**: GPU acceleration, batched derivatives
3. **IDA Solver**: C++ wrapper, sparse Jacobian support
4. **LRU Caching**: Avoid recomputation
%% Parsing uses tree-sitter workers
parsing --> worker_pool --> parse_worker
parse_worker --> parser_loader
parse_worker --> ts_queries
---
%% Ingestion → Storage
processes --> csv_gen --> kuzu
processes --> embedder
## 🔐 Testing Strategy
%% MCP flow
mcp_cmd --> server --> tools --> backend --> kuzu
server --> resources --> backend
backend --> bm25
backend --> semantic --> embedder
### Unit Tests (973 files)
- Component-level validation
- Expression tree operations
- Spatial method correctness
%% CLI → Setup
setup --> repo_mgr
setup --> claude_hook
setup --> cursor_hook
setup --> skills
### Integration Tests
- Full model runs
- Solver convergence
- Different parameter sets
%% Hooks → MCP
claude_hook -.->|augments searches| tools
cursor_hook -.->|augments searches| tools
augment -.->|fast CLI path| backend
### Benchmark Tests
- Performance tracking
- Memory profiling
- Scaling analysis
%% Web
app --> web_worker --> components
web_worker --> kuzu
---
## 📚 Key Math Concepts
### Governing Equations
**DAE System:**
```
M(t,y) * dy/dt = f(t, y, u(t)) [Differential equations]
0 = g(t, y, u(t)) [Algebraic equations]
%% Serve
serve --> backend
```
where:
- `y` = state vector (concentrations, potentials, temperature)
- `u(t)` = inputs (applied current, ambient temperature)
- `M` = mass matrix (handles singular systems)
## Data Flow Summary
### Typical Physics
**Particle Diffusion (Fick's Law):**
```
∂c/∂t = ∇·(D∇c)
Source Code
┌─────────────────────────────────────────┐
│ Ingestion Pipeline (8 phases) │
│ Files → AST → Symbols → Relationships │
│ → Communities → Execution Flows │
└─────────────────┬───────────────────────┘
┌───────┴───────┐
▼ ▼
┌──────────┐ ┌────────────┐
│ KuzuDB │ │ Embeddings │
│ (graph) │ │ (vectors) │
└────┬─────┘ └─────┬──────┘
│ │
└───────┬───────┘
┌──────────────┐
│ MCP Server │
│ (7 tools) │
└──────┬───────┘
┌───────────┼───────────┐
▼ ▼ ▼
Claude Cursor Other
Code Editor MCP Clients
```
**Charge Conservation (Poisson):**
```
∇·(σ∇φ) = i
```
**Energy Balance (Heat Equation):**
```
ρCp ∂T/∂t = ∇·(k∇T) + Q_gen
```
---
## 🎓 Learning Path
1. **Start**: Run SPM model (`pybamm.lithium_ion.SPM()`)
2. **Progress**: Modify parameter set, change solver
3. **Intermediate**: Swap submodels (DFN, thermal)
4. **Advanced**: Create custom submodel
5. **Expert**: Implement new spatial method
---
## 🔮 Architecture Strengths
**Modularity**: Plug-and-play submodels
**Extensibility**: Easy to add new models/solvers
**Physics-First**: Expression tree mirrors actual equations
**Backend-Agnostic**: Switch solvers without changing model
**Scientific Quality**: Validated against experiments
**Performance**: Multiple backends (Python, C++, JAX)
---
## ⚠️ Architecture Tradeoffs
⚖️ **Complexity**: Large learning curve
⚖️ **Symbolic Overhead**: DAG construction has memory cost
⚖️ **Debug Difficulty**: Multiple abstraction layers
⚖️ **Startup Time**: Model compilation + discretisation
---
## 🎯 Conclusion
PyBaMM's architecture is a **layered, modular system** optimized for:
- **Scientific fidelity** (physics-based discretisation)
- **Extensibility** (plug-and-play submodels)
- **Performance** (multiple backends)
- **Usability** (high-level simulation API)
The design cleanly separates concerns across 7 layers, from symbolic math to numerical solvers, making it suitable for both research and production use.
---
*Analysis powered by GitNexus MCP - Code Intelligence Engine*
## Supported Languages
Tree-sitter grammars are included for: **TypeScript**, **JavaScript**, **Python**, **Java**, **C**, **C++**, **C#**, **Go**, **Rust**.
## Key Design Decisions
1. **Augmentation over replacement** — Hooks enrich existing AI agent tools (Grep, Glob, Bash) with graph context rather than replacing them
2. **Native tree-sitter** — Uses N-API bindings (not WASM) in the CLI for performance; WASM in the browser
3. **Worker thread parsing** — CPU-bound tree-sitter parsing parallelized across `cpus - 1` worker threads
4. **Hybrid search** — BM25 keyword + semantic vector search combined with Reciprocal Rank Fusion for ranking
5. **LRU AST cache** — Parsed trees are cached across pipeline phases to avoid redundant re-parsing
6. **Deterministic IDs**`generateId(label, qualifiedName)` ensures idempotent graph construction

View file

@ -374,3 +374,11 @@ model = pybamm.lithium_ion.DFN(

62
CLAUDE.md Normal file
View file

@ -0,0 +1,62 @@
<!-- gitnexus:start -->
# GitNexus MCP
This project is indexed by GitNexus as **GitnexusV2** (1295 symbols, 3262 relationships, 99 execution flows).
GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
## Always Start Here
For any task involving code understanding, debugging, impact analysis, or refactoring, you must:
1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
## Skills
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/refactoring/SKILL.md` |
## Tools Reference
| Tool | What it gives you |
|------|-------------------|
| `query` | Process-grouped code intelligence — execution flows related to a concept |
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
| `detect_changes` | Git-diff impact — what do your current changes affect |
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
| `list_repos` | Discover indexed repos |
## Resources Reference
Lightweight reads (~100-500 tokens) for navigation:
| Resource | Content |
|----------|---------|
| `gitnexus://repo/{name}/context` | Stats, staleness check |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
| `gitnexus://repo/{name}/processes` | All execution flows |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |
## Graph Schema
**Nodes:** File, Function, Class, Interface, Method, Community, Process
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
RETURN caller.name, caller.filePath
```
<!-- gitnexus:end -->

View file

@ -373,3 +373,11 @@ Expression tree traversal:

View file

@ -18,7 +18,7 @@ The licensor grants you an additional copyright license to distribute copies of
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
> Required Notice: Copyright Abhigyan Patwari (https://github.com/abhigyanpatwari/GitNexus)
## Changes and New Works License

861
README.md

File diff suppressed because it is too large Load diff

23
eval/.env.example Normal file
View file

@ -0,0 +1,23 @@
# ─── GitNexus SWE-bench Eval — API Keys ───
# Copy this file to .env and fill in the keys you have.
# You only need keys for the models you plan to test.
# OpenRouter (covers Claude, MiniMax, GLM, and 200+ other models)
# Get yours at: https://openrouter.ai/keys
OPENROUTER_API_KEY=
# Anthropic (direct — optional if using OpenRouter)
# Get yours at: https://console.anthropic.com/
ANTHROPIC_API_KEY=
# ZhipuAI / GLM (direct — optional if using OpenRouter)
# Get yours at: https://open.bigmodel.cn/
ZHIPUAI_API_KEY=
# MiniMax (direct — optional if using OpenRouter)
MINIMAX_API_KEY=
# ─── Optional ───
# Cost tracking: set to "ignore_errors" if litellm can't find pricing for a model
# MSWEA_COST_TRACKING=ignore_errors

16
eval/.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
# Evaluation results (large, should not be committed)
results/
*.traj.json
preds.json
# Python
__pycache__/
*.pyc
*.egg-info/
.eggs/
dist/
build/
# Environment
.env
.venv/

210
eval/README.md Normal file
View file

@ -0,0 +1,210 @@
# GitNexus SWE-bench Evaluation Harness
Evaluate whether GitNexus code intelligence improves AI agent performance on real software engineering tasks. Runs SWE-bench instances across multiple models and compares baseline (no graph) vs GitNexus-enhanced configurations.
## What This Tests
**Hypothesis**: Giving AI agents structural code intelligence (call graphs, execution flows, blast radius analysis) improves their ability to resolve real GitHub issues — measured by resolve rate, cost, and efficiency.
**Evaluation modes:**
| Mode | What the agent gets |
|------|-------------------|
| `baseline` | Standard bash tools (grep, find, cat, sed) — control group |
| `native` | Baseline + explicit GitNexus tools via eval-server (~100ms) |
| `native_augment` | Native tools + grep results automatically enriched with graph context (**recommended**) |
> **Recommended**: Use `native_augment` mode. It mirrors the Claude Code model — the agent gets both explicit GitNexus tools (fast bash commands) AND automatic enrichment of grep results with callers, callees, and execution flows. The agent decides when to use explicit tools vs rely on enriched search output.
**Models supported:**
- Claude 3.5 Haiku, Claude Sonnet 4, Claude Opus 4
- MiniMax M1 2.5
- GLM 4.7, GLM 5
- Any model supported by litellm (add a YAML config)
## Prerequisites
- Python 3.11+
- Docker (for SWE-bench containers)
- Node.js 18+ (for GitNexus)
- API keys for your chosen models
## Setup
```bash
cd eval
# Install dependencies
pip install -e .
# Set up API keys — copy the template and fill in your keys
cp .env.example .env
# Then edit .env and paste your key(s)
```
All models are routed through **OpenRouter** by default, so a single `OPENROUTER_API_KEY` is all you need. To use provider APIs directly (Anthropic, ZhipuAI, etc.), edit the model YAML in `configs/models/` and set the corresponding key in `.env`.
```bash
# Pull SWE-bench Docker images (pulled on-demand, but you can pre-pull)
docker pull swebench/sweb.eval.x86_64.django_1776_django-16527:latest
```
## Quick Start
### Debug a single instance
```bash
# Fastest way to verify everything works
python run_eval.py debug -m claude-haiku -i django__django-16527 --subset lite
```
### Run a single configuration
```bash
# 5 instances, Claude Sonnet, native_augment mode (default)
python run_eval.py single -m claude-sonnet --subset lite --slice 0:5
# Baseline comparison (no GitNexus)
python run_eval.py single -m claude-sonnet --mode baseline --subset lite --slice 0:5
# Full Lite benchmark, 4 parallel workers
python run_eval.py single -m claude-sonnet --subset lite -w 4
```
### Run the full matrix
```bash
# All models x all modes
python run_eval.py matrix --subset lite -w 4
# Key comparison: baseline vs native_augment
python run_eval.py matrix -m claude-sonnet -m claude-haiku --modes baseline --modes native_augment --subset lite --slice 0:50
```
### Analyze results
```bash
# Summary table
python -m analysis.analyze_results results/
# Compare modes for a specific model
python -m analysis.analyze_results compare-modes results/ -m claude-sonnet
# GitNexus tool usage analysis
python -m analysis.analyze_results gitnexus-usage results/
# Export as CSV for further analysis
python -m analysis.analyze_results summary results/ --format csv > results.csv
# Run official SWE-bench test evaluation
python -m analysis.analyze_results summary results/ --swebench-eval
```
### List available configurations
```bash
python run_eval.py list-configs
```
## Architecture
```
eval/
run_eval.py # Main entry point (single, matrix, debug commands)
agents/
gitnexus_agent.py # GitNexusAgent: extends DefaultAgent with augmentation + metrics
environments/
gitnexus_docker.py # Docker env with GitNexus + eval-server + standalone tool scripts
bridge/
gitnexus_tools.sh # Bash wrappers (legacy — now standalone scripts are installed directly)
mcp_bridge.py # Legacy MCP bridge (kept for reference)
prompts/
system_baseline.jinja # System: persona + format rules
instance_baseline.jinja # Instance: task + workflow
system_native.jinja # System: + GitNexus tool reference
instance_native.jinja # Instance: + GitNexus debugging workflow
system_native_augment.jinja # System: + GitNexus tools + grep enrichment docs
instance_native_augment.jinja # Instance: + GitNexus workflow + risk assessment
configs/
models/ # Per-model YAML configs
modes/ # Per-mode YAML configs (baseline, native, native_augment)
analysis/
analyze_results.py # Post-run comparative analysis
results/ # Output directory (gitignored)
```
## How It Works
### Template structure
mini-swe-agent requires two Jinja templates:
- **system_template** → system message: persona, format rules, tool reference (static)
- **instance_template** → first user message: task, workflow, rules, examples (contains `{{task}}`)
Each mode has a `system_{mode}.jinja` + `instance_{mode}.jinja` pair. The agent loads both automatically based on the configured mode.
### Per-instance flow
1. Docker container starts with SWE-bench instance (repo at specific commit)
2. **GitNexus setup**: Node.js + gitnexus installed, `gitnexus analyze` runs (or restores from cache)
3. **Eval-server starts**: `gitnexus eval-server` daemon (persistent HTTP server, keeps KuzuDB warm)
4. **Standalone tool scripts installed** in `/usr/local/bin/` — works with `subprocess.run` (no `.bashrc` needed)
5. Agent runs with the configured model + system prompt + GitNexus tools
6. Agent's patch is extracted as a git diff
7. Metrics collected: cost, tokens, tool calls, GitNexus usage, augmentation stats
### Tool architecture
```
Agent → bash command → /usr/local/bin/gitnexus-query
→ curl localhost:4848/tool/query (fast path: eval-server, ~100ms)
→ npx gitnexus query (fallback: cold CLI, ~5-10s)
```
Each tool script in `/usr/local/bin/` is standalone — no sourcing, no env inheritance needed. This is critical because mini-swe-agent runs every command via `subprocess.run` in a fresh subshell.
### Eval-server
The eval-server is a lightweight HTTP daemon that:
- Keeps KuzuDB warm in memory (no cold start per tool call)
- Returns LLM-friendly text (not raw JSON — saves tokens)
- Includes next-step hints to guide tool chaining (query → context → impact → fix)
- Auto-shuts down after idle timeout
### Index caching
SWE-bench repos repeat (Django has 200+ instances at different commits). The harness caches GitNexus indexes per `(repo, commit)` hash in `~/.gitnexus-eval-cache/` to avoid redundant re-indexing.
### Grep augmentation (native_augment mode)
When the agent runs `grep` or `rg`, the observation is post-processed: the agent class calls `gitnexus-augment` on the search pattern and appends `[GitNexus]` annotations showing callers, callees, and execution flows for matched symbols. This mirrors the Claude Code / Cursor hook integration.
## Adding Models
Create a YAML file in `configs/models/`:
```yaml
# configs/models/my-model.yaml
model:
model_name: "openrouter/provider/model-name"
cost_tracking: "ignore_errors" # if not in litellm's cost DB
model_kwargs:
max_tokens: 8192
temperature: 0
```
The model name follows [litellm conventions](https://docs.litellm.ai/docs/providers).
## Metrics Collected
| Metric | Description |
|--------|-------------|
| Patch Rate | % of instances where agent produced a patch |
| Resolve Rate | % of instances where patch passes tests (requires --swebench-eval) |
| Total Cost | API cost across all instances |
| Avg Cost/Instance | Cost efficiency |
| API Calls | Number of LLM calls |
| GN Tool Calls | How many GitNexus tools the agent used |
| Augment Hits | How many grep/find results got enriched |
| Augment Hit Rate | % of search commands that got useful enrichment |

1
eval/__init__.py Normal file
View file

@ -0,0 +1 @@
# GitNexus SWE-bench Evaluation Harness

0
eval/agents/__init__.py Normal file
View file

View file

@ -0,0 +1,209 @@
"""
GitNexus-Enhanced Agent for SWE-bench Evaluation
Extends mini-swe-agent's DefaultAgent with:
1. Native augment mode: GitNexus tools via eval-server + grep enrichment (recommended)
2. Native mode: GitNexus tools via eval-server only
3. Baseline mode: Pure mini-swe-agent (no GitNexus control group)
The agent class itself is minimal the heavy lifting is in:
- Prompt selection (system + instance templates per mode)
- Observation post-processing (grep result augmentation)
- Metrics tracking (which tools the agent actually uses)
Template structure (matches mini-swe-agent's expectations):
system_template system message: persona + format rules + tool reference
instance_template first user message: task + workflow + rules + examples
"""
import logging
import re
import time
from enum import Enum
from pathlib import Path
from minisweagent import Environment, Model
from minisweagent.agents.default import AgentConfig, DefaultAgent
logger = logging.getLogger("gitnexus_agent")
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
class GitNexusMode(str, Enum):
"""Evaluation modes for GitNexus integration."""
BASELINE = "baseline" # No GitNexus — pure mini-swe-agent
NATIVE = "native" # GitNexus tools via eval-server
NATIVE_AUGMENT = "native_augment" # Native tools + grep enrichment (recommended)
class GitNexusAgentConfig(AgentConfig):
"""Extended config for GitNexus evaluation agent."""
gitnexus_mode: GitNexusMode = GitNexusMode.BASELINE
augment_timeout: float = 5.0
augment_min_pattern_length: int = 3
track_gitnexus_usage: bool = True
class GitNexusAgent(DefaultAgent):
"""
Agent that optionally enriches its capabilities with GitNexus code intelligence.
In BASELINE mode, behaves identically to DefaultAgent.
In NATIVE mode, GitNexus tools are available as bash commands via eval-server.
In NATIVE_AUGMENT mode, GitNexus tools + automatic grep result enrichment.
"""
def __init__(self, model: Model, env: Environment, *, config_class: type = GitNexusAgentConfig, **kwargs):
mode = kwargs.get("gitnexus_mode", GitNexusMode.BASELINE)
if isinstance(mode, str):
mode = GitNexusMode(mode)
# Load system template
system_file = PROMPTS_DIR / f"system_{mode.value}.jinja"
if system_file.exists() and "system_template" not in kwargs:
kwargs["system_template"] = system_file.read_text()
# Load instance template
instance_file = PROMPTS_DIR / f"instance_{mode.value}.jinja"
if instance_file.exists() and "instance_template" not in kwargs:
kwargs["instance_template"] = instance_file.read_text()
super().__init__(model, env, config_class=config_class, **kwargs)
self.gitnexus_mode = mode
self.gitnexus_metrics = GitNexusMetrics()
def execute_actions(self, message: dict) -> list[dict]:
"""Execute actions with optional GitNexus augmentation and tracking."""
if self.config.track_gitnexus_usage:
self._track_tool_usage(message)
outputs = [self.env.execute(action) for action in message.get("extra", {}).get("actions", [])]
# Augment grep/find observations in NATIVE_AUGMENT mode
if self.gitnexus_mode == GitNexusMode.NATIVE_AUGMENT:
actions = message.get("extra", {}).get("actions", [])
for i, (action, output) in enumerate(zip(actions, outputs)):
augmented = self._maybe_augment(action, output)
if augmented:
outputs[i] = augmented
return self.add_messages(
*self.model.format_observation_messages(message, outputs, self.get_template_vars())
)
def _maybe_augment(self, action: dict, output: dict) -> dict | None:
"""
If the action is a search command (grep, find, rg, ag), augment the output
with GitNexus knowledge graph context.
"""
command = action.get("command", "")
if not command:
return None
pattern = self._extract_search_pattern(command)
if not pattern or len(pattern) < self.config.augment_min_pattern_length:
return None
start = time.time()
try:
augment_result = self.env.execute({
"command": f'gitnexus-augment "{pattern}" 2>&1 || true',
"timeout": self.config.augment_timeout,
})
elapsed = time.time() - start
self.gitnexus_metrics.augmentation_calls += 1
self.gitnexus_metrics.augmentation_time += elapsed
augment_text = augment_result.get("output", "").strip()
if augment_text and "[GitNexus]" in augment_text:
original_output = output.get("output", "")
output = dict(output)
output["output"] = f"{original_output}\n\n{augment_text}"
self.gitnexus_metrics.augmentation_hits += 1
return output
except Exception as e:
logger.debug(f"Augmentation failed for pattern '{pattern}': {e}")
self.gitnexus_metrics.augmentation_errors += 1
return None
@staticmethod
def _extract_search_pattern(command: str) -> str | None:
"""Extract the search pattern from a grep/find/rg command."""
patterns = [
r'(?:grep|rg|ag)\s+(?:-[a-zA-Z]*\s+)*["\']([^"\']+)["\']',
r'(?:grep|rg|ag)\s+(?:-[a-zA-Z]*\s+)*(\S+)',
]
for pat in patterns:
match = re.search(pat, command)
if match:
result = match.group(1)
if result.startswith("/") or result.startswith("."):
continue
if result.startswith("-"):
continue
return result
return None
def _track_tool_usage(self, message: dict):
"""Track which GitNexus tools the agent uses."""
for action in message.get("extra", {}).get("actions", []):
command = action.get("command", "")
if "gitnexus-query" in command:
self.gitnexus_metrics.tool_calls["query"] += 1
elif "gitnexus-context" in command:
self.gitnexus_metrics.tool_calls["context"] += 1
elif "gitnexus-impact" in command:
self.gitnexus_metrics.tool_calls["impact"] += 1
elif "gitnexus-cypher" in command:
self.gitnexus_metrics.tool_calls["cypher"] += 1
elif "gitnexus-overview" in command:
self.gitnexus_metrics.tool_calls["overview"] += 1
def serialize(self, *extra_dicts) -> dict:
"""Serialize with GitNexus-specific metrics."""
gitnexus_data = {
"info": {
"gitnexus": {
"mode": self.gitnexus_mode.value,
"metrics": self.gitnexus_metrics.to_dict(),
},
},
}
return super().serialize(gitnexus_data, *extra_dicts)
class GitNexusMetrics:
"""Tracks GitNexus-specific metrics during evaluation."""
def __init__(self):
self.tool_calls: dict[str, int] = {
"query": 0,
"context": 0,
"impact": 0,
"cypher": 0,
"overview": 0,
}
self.augmentation_calls: int = 0
self.augmentation_hits: int = 0
self.augmentation_errors: int = 0
self.augmentation_time: float = 0.0
self.index_time: float = 0.0
@property
def total_tool_calls(self) -> int:
return sum(self.tool_calls.values())
def to_dict(self) -> dict:
return {
"tool_calls": dict(self.tool_calls),
"total_tool_calls": self.total_tool_calls,
"augmentation_calls": self.augmentation_calls,
"augmentation_hits": self.augmentation_hits,
"augmentation_errors": self.augmentation_errors,
"augmentation_time_seconds": round(self.augmentation_time, 2),
"index_time_seconds": round(self.index_time, 2),
}

View file

View file

@ -0,0 +1,445 @@
#!/usr/bin/env python3
"""
Results Analyzer for GitNexus SWE-bench Evaluation
Reads evaluation results and generates comparative analysis:
- Resolve rate by model x mode
- Cost comparison (total, per-instance)
- Token/API call efficiency
- GitNexus tool usage patterns
- Augmentation hit rates
Usage:
python -m analysis.analyze_results /path/to/results
python -m analysis.analyze_results /path/to/results --format markdown
python -m analysis.analyze_results /path/to/results --swebench-eval # run actual test verification
"""
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Any
import typer
from rich.console import Console
from rich.table import Table
logger = logging.getLogger("analyze_results")
console = Console()
app = typer.Typer(rich_markup_mode="rich", add_completion=False)
def load_run_results(results_dir: Path) -> dict[str, dict]:
"""
Load all run results from the results directory.
Returns: {run_id: {summary, preds, instances}}
"""
runs = {}
for run_dir in sorted(results_dir.iterdir()):
if not run_dir.is_dir():
continue
run_id = run_dir.name
run_data: dict[str, Any] = {"run_id": run_id, "dir": run_dir}
# Load summary
summary_path = run_dir / "summary.json"
if summary_path.exists():
run_data["summary"] = json.loads(summary_path.read_text())
# Load predictions
preds_path = run_dir / "preds.json"
if preds_path.exists():
run_data["preds"] = json.loads(preds_path.read_text())
# Load individual trajectories for detailed metrics
run_data["trajectories"] = {}
for traj_dir in run_dir.iterdir():
if not traj_dir.is_dir():
continue
for traj_file in traj_dir.glob("*.traj.json"):
try:
traj = json.loads(traj_file.read_text())
instance_id = traj.get("instance_id", traj_dir.name)
run_data["trajectories"][instance_id] = traj
except Exception:
pass
if run_data.get("preds") or run_data.get("summary"):
runs[run_id] = run_data
return runs
def parse_run_id(run_id: str) -> tuple[str, str]:
"""Parse 'model_mode' into (model, mode)."""
# Handle multi-word model names like 'minimax-2.5'
# Modes are: baseline, mcp, augment, full
known_modes = {"baseline", "mcp", "augment", "full"}
parts = run_id.rsplit("_", 1)
if len(parts) == 2 and parts[1] in known_modes:
return parts[0], parts[1]
return run_id, "unknown"
def compute_metrics(run_data: dict) -> dict:
"""Compute evaluation metrics for a single run."""
preds = run_data.get("preds", {})
summary = run_data.get("summary", {})
trajectories = run_data.get("trajectories", {})
n_instances = len(preds)
n_with_patch = sum(1 for p in preds.values() if p.get("model_patch", "").strip())
# Cost and API call metrics from trajectories
costs = []
api_calls = []
gn_tool_calls = []
gn_augment_hits = []
gn_augment_calls = []
for instance_id, traj in trajectories.items():
info = traj.get("info", {})
model_stats = info.get("model_stats", {})
costs.append(model_stats.get("instance_cost", 0))
api_calls.append(model_stats.get("api_calls", 0))
gn = info.get("gitnexus", {}).get("metrics", {})
if gn:
gn_tool_calls.append(gn.get("total_tool_calls", 0))
gn_augment_hits.append(gn.get("augmentation_hits", 0))
gn_augment_calls.append(gn.get("augmentation_calls", 0))
# Also try summary-level metrics
if not costs and summary:
results = summary.get("results", [])
for r in results:
costs.append(r.get("cost", 0))
api_calls.append(r.get("n_calls", 0))
gn = r.get("gitnexus_metrics", {})
if gn:
gn_tool_calls.append(gn.get("total_tool_calls", 0))
gn_augment_hits.append(gn.get("augmentation_hits", 0))
gn_augment_calls.append(gn.get("augmentation_calls", 0))
total_cost = sum(costs)
total_calls = sum(api_calls)
return {
"n_instances": n_instances,
"n_with_patch": n_with_patch,
"patch_rate": n_with_patch / max(n_instances, 1),
"total_cost": total_cost,
"avg_cost": total_cost / max(n_instances, 1),
"total_api_calls": total_calls,
"avg_api_calls": total_calls / max(n_instances, 1),
"total_gn_tool_calls": sum(gn_tool_calls),
"avg_gn_tool_calls": sum(gn_tool_calls) / max(len(gn_tool_calls), 1) if gn_tool_calls else 0,
"total_augment_hits": sum(gn_augment_hits),
"total_augment_calls": sum(gn_augment_calls),
"augment_hit_rate": sum(gn_augment_hits) / max(sum(gn_augment_calls), 1) if gn_augment_calls else 0,
}
def run_swebench_evaluation(results_dir: Path, run_id: str, subset: str = "lite") -> dict | None:
"""
Run the official SWE-bench evaluation on predictions.
Requires: pip install swebench
"""
preds_path = results_dir / run_id / "preds.json"
if not preds_path.exists():
return None
dataset_mapping = {
"lite": "princeton-nlp/SWE-Bench_Lite",
"verified": "princeton-nlp/SWE-Bench_Verified",
"full": "princeton-nlp/SWE-Bench",
}
try:
eval_output = results_dir / run_id / "swebench_eval"
cmd = [
"python", "-m", "swebench.harness.run_evaluation",
"--dataset_name", dataset_mapping.get(subset, subset),
"--predictions_path", str(preds_path),
"--max_workers", "4",
"--run_id", run_id,
"--output_dir", str(eval_output),
]
logger.info(f"Running SWE-bench evaluation for {run_id}...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
# Parse evaluation results
report_path = eval_output / run_id / "results.json"
if report_path.exists():
return json.loads(report_path.read_text())
logger.error(f"SWE-bench eval failed: {result.stderr[:500]}")
return None
except Exception as e:
logger.error(f"SWE-bench eval error: {e}")
return None
# ─── CLI Commands ───────────────────────────────────────────────────────────
@app.command()
def summary(
results_dir: str = typer.Argument(..., help="Path to results directory"),
format: str = typer.Option("table", "--format", help="Output format: table, markdown, json, csv"),
swebench_eval: bool = typer.Option(False, "--swebench-eval", help="Run official SWE-bench test evaluation"),
subset: str = typer.Option("lite", "--subset", help="SWE-bench subset (for --swebench-eval)"),
):
"""Generate comparative analysis of evaluation results."""
results_path = Path(results_dir)
if not results_path.exists():
console.print(f"[red]Results directory not found: {results_path}[/red]")
raise typer.Exit(1)
runs = load_run_results(results_path)
if not runs:
console.print("[yellow]No evaluation results found[/yellow]")
raise typer.Exit(0)
console.print(f"\n[bold]Found {len(runs)} evaluation runs[/bold]\n")
# Compute metrics per run
all_metrics = {}
for run_id, run_data in runs.items():
model, mode = parse_run_id(run_id)
metrics = compute_metrics(run_data)
metrics["model"] = model
metrics["mode"] = mode
# Optionally run SWE-bench evaluation
if swebench_eval:
eval_result = run_swebench_evaluation(results_path, run_id, subset)
if eval_result:
metrics["resolved"] = eval_result.get("resolved", 0)
metrics["resolve_rate"] = eval_result.get("resolved", 0) / max(metrics["n_instances"], 1)
all_metrics[run_id] = metrics
if format == "table":
_print_table(all_metrics)
elif format == "markdown":
_print_markdown(all_metrics)
elif format == "json":
console.print(json.dumps(all_metrics, indent=2))
elif format == "csv":
_print_csv(all_metrics)
@app.command()
def compare_modes(
results_dir: str = typer.Argument(..., help="Path to results directory"),
model: str = typer.Option(..., "-m", "--model", help="Model to compare across modes"),
):
"""Compare modes for a specific model (baseline vs mcp vs augment vs full)."""
results_path = Path(results_dir)
runs = load_run_results(results_path)
# Filter to the specified model
model_runs = {
run_id: data for run_id, data in runs.items()
if parse_run_id(run_id)[0] == model
}
if not model_runs:
console.print(f"[yellow]No results found for model: {model}[/yellow]")
raise typer.Exit(1)
console.print(f"\n[bold]Mode comparison for {model}[/bold]\n")
metrics = {}
for run_id, run_data in model_runs.items():
_, mode = parse_run_id(run_id)
metrics[mode] = compute_metrics(run_data)
# Print comparison table
table = Table(title=f"Mode Comparison: {model}")
table.add_column("Metric", style="bold")
for mode in ["baseline", "mcp", "augment", "full"]:
if mode in metrics:
table.add_column(mode, justify="right")
rows = [
("Instances", "n_instances", "d"),
("With Patch", "n_with_patch", "d"),
("Patch Rate", "patch_rate", ".1%"),
("Total Cost", "total_cost", "$.4f"),
("Avg Cost", "avg_cost", "$.4f"),
("Total API Calls", "total_api_calls", "d"),
("Avg API Calls", "avg_api_calls", ".1f"),
("GN Tool Calls", "total_gn_tool_calls", "d"),
("Augment Hits", "total_augment_hits", "d"),
("Augment Hit Rate", "augment_hit_rate", ".1%"),
]
for label, key, fmt in rows:
values = []
for mode in ["baseline", "mcp", "augment", "full"]:
if mode in metrics:
v = metrics[mode].get(key, 0)
if fmt == ".1%":
values.append(f"{v:.1%}")
elif fmt == "$.4f":
values.append(f"${v:.4f}")
elif fmt == ".1f":
values.append(f"{v:.1f}")
else:
values.append(str(v))
table.add_row(label, *values)
# Add delta rows (improvement over baseline)
if "baseline" in metrics:
baseline_cost = metrics["baseline"]["avg_cost"]
baseline_calls = metrics["baseline"]["avg_api_calls"]
table.add_section()
for mode in ["mcp", "augment", "full"]:
if mode not in metrics:
continue
mode_cost = metrics[mode]["avg_cost"]
mode_calls = metrics[mode]["avg_api_calls"]
cost_delta = ((mode_cost - baseline_cost) / max(baseline_cost, 0.001)) * 100
calls_delta = ((mode_calls - baseline_calls) / max(baseline_calls, 1)) * 100
cost_str = f"{cost_delta:+.1f}%"
calls_str = f"{calls_delta:+.1f}%"
# Color-code: negative is good (cheaper/fewer calls)
cost_color = "green" if cost_delta < 0 else "red"
calls_color = "green" if calls_delta < 0 else "red"
console.print(f" {mode} vs baseline: cost [{cost_color}]{cost_str}[/{cost_color}], calls [{calls_color}]{calls_str}[/{calls_color}]")
console.print(table)
@app.command()
def gitnexus_usage(
results_dir: str = typer.Argument(..., help="Path to results directory"),
):
"""Analyze GitNexus tool usage patterns across all runs."""
results_path = Path(results_dir)
runs = load_run_results(results_path)
console.print("\n[bold]GitNexus Tool Usage Analysis[/bold]\n")
table = Table(title="Tool Usage by Run")
table.add_column("Run", style="bold")
table.add_column("query", justify="right")
table.add_column("context", justify="right")
table.add_column("impact", justify="right")
table.add_column("cypher", justify="right")
table.add_column("Total", justify="right")
table.add_column("Augment Hits", justify="right")
for run_id, run_data in sorted(runs.items()):
_, mode = parse_run_id(run_id)
if mode == "baseline":
continue
# Aggregate tool calls across trajectories
tool_totals: dict[str, int] = {"query": 0, "context": 0, "impact": 0, "cypher": 0, "overview": 0}
augment_hits = 0
for traj in run_data.get("trajectories", {}).values():
gn = traj.get("info", {}).get("gitnexus", {}).get("metrics", {})
for tool, count in gn.get("tool_calls", {}).items():
tool_totals[tool] = tool_totals.get(tool, 0) + count
augment_hits += gn.get("augmentation_hits", 0)
# Also check summary
for r in run_data.get("summary", {}).get("results", []):
gn = r.get("gitnexus_metrics", {})
for tool, count in gn.get("tool_calls", {}).items():
tool_totals[tool] = tool_totals.get(tool, 0) + count
augment_hits += gn.get("augmentation_hits", 0)
total = sum(tool_totals.values())
if total > 0 or augment_hits > 0:
table.add_row(
run_id,
str(tool_totals.get("query", 0)),
str(tool_totals.get("context", 0)),
str(tool_totals.get("impact", 0)),
str(tool_totals.get("cypher", 0)),
str(total),
str(augment_hits),
)
console.print(table)
# ─── Output Formatters ─────────────────────────────────────────────────────
def _print_table(all_metrics: dict):
"""Print rich table summary."""
table = Table(title="Evaluation Results")
table.add_column("Run", style="bold")
table.add_column("Model")
table.add_column("Mode")
table.add_column("N", justify="right")
table.add_column("Patched", justify="right")
table.add_column("Rate", justify="right")
table.add_column("Cost", justify="right")
table.add_column("Calls", justify="right")
table.add_column("GN Tools", justify="right")
for run_id, m in sorted(all_metrics.items()):
resolved_str = ""
if "resolve_rate" in m:
resolved_str = f" ({m['resolve_rate']:.0%})"
table.add_row(
run_id,
m["model"],
m["mode"],
str(m["n_instances"]),
str(m["n_with_patch"]),
f"{m['patch_rate']:.0%}{resolved_str}",
f"${m['total_cost']:.2f}",
str(m["total_api_calls"]),
str(m["total_gn_tool_calls"]) if m["total_gn_tool_calls"] > 0 else "-",
)
console.print(table)
def _print_markdown(all_metrics: dict):
"""Print markdown table."""
print("| Run | Model | Mode | N | Patched | Rate | Cost | Calls | GN Tools |")
print("|-----|-------|------|---|---------|------|------|-------|----------|")
for run_id, m in sorted(all_metrics.items()):
gn = str(m["total_gn_tool_calls"]) if m["total_gn_tool_calls"] > 0 else "-"
print(f"| {run_id} | {m['model']} | {m['mode']} | {m['n_instances']} | {m['n_with_patch']} | {m['patch_rate']:.0%} | ${m['total_cost']:.2f} | {m['total_api_calls']} | {gn} |")
def _print_csv(all_metrics: dict):
"""Print CSV output."""
print("run_id,model,mode,n_instances,n_with_patch,patch_rate,total_cost,avg_cost,total_api_calls,avg_api_calls,total_gn_tool_calls,total_augment_hits,augment_hit_rate")
for run_id, m in sorted(all_metrics.items()):
print(
f"{run_id},{m['model']},{m['mode']},{m['n_instances']},{m['n_with_patch']},"
f"{m['patch_rate']:.4f},{m['total_cost']:.4f},{m['avg_cost']:.4f},"
f"{m['total_api_calls']},{m['avg_api_calls']:.1f},{m['total_gn_tool_calls']},"
f"{m['total_augment_hits']},{m['augment_hit_rate']:.4f}"
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
app()

0
eval/bridge/__init__.py Normal file
View file

View file

@ -0,0 +1,155 @@
#!/bin/bash
# GitNexus CLI tool wrappers for SWE-bench evaluation
#
# These functions call the GitNexus eval-server (HTTP daemon) for near-instant
# tool responses. The eval-server keeps KuzuDB warm in memory.
#
# If the eval-server is not running, falls back to direct CLI commands.
#
# Usage:
# gitnexus-query "how does authentication work"
# gitnexus-context "validateUser"
# gitnexus-impact "AuthService" upstream
# gitnexus-cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
# gitnexus-overview
GITNEXUS_EVAL_PORT="${GITNEXUS_EVAL_PORT:-4848}"
GITNEXUS_EVAL_URL="http://127.0.0.1:${GITNEXUS_EVAL_PORT}"
_gitnexus_call() {
local tool="$1"
shift
local json_body="$1"
# Try eval-server first (fastest path — KuzuDB stays warm)
local result
result=$(curl -sf -X POST "${GITNEXUS_EVAL_URL}/tool/${tool}" \
-H "Content-Type: application/json" \
-d "${json_body}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
echo "$result"
return 0
fi
# Fallback: direct CLI (cold start, slower but always works)
case "$tool" in
query)
local q=$(echo "$json_body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('query',''))" 2>/dev/null)
npx gitnexus query "$q" 2>&1
;;
context)
local n=$(echo "$json_body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null)
npx gitnexus context "$n" 2>&1
;;
impact)
local t=$(echo "$json_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('target',''))" 2>/dev/null)
local d=$(echo "$json_body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('direction','upstream'))" 2>/dev/null)
npx gitnexus impact "$t" --direction "$d" 2>&1
;;
cypher)
local cq=$(echo "$json_body" | python3 -c "import sys,json; print(json.load(sys.stdin).get('query',''))" 2>/dev/null)
npx gitnexus cypher "$cq" 2>&1
;;
*)
echo "Unknown tool: $tool" >&2
return 1
;;
esac
}
gitnexus-query() {
local query="$1"
local task_context="${2:-}"
local goal="${3:-}"
if [ -z "$query" ]; then
echo "Usage: gitnexus-query <query> [task_context] [goal]"
echo "Search the code knowledge graph for execution flows related to a concept."
echo ""
echo "Examples:"
echo ' gitnexus-query "authentication flow"'
echo ' gitnexus-query "database connection" "fixing connection pool leak"'
return 1
fi
local args="{\"query\": \"$query\""
[ -n "$task_context" ] && args="$args, \"task_context\": \"$task_context\""
[ -n "$goal" ] && args="$args, \"goal\": \"$goal\""
args="$args}"
_gitnexus_call query "$args"
}
gitnexus-context() {
local name="$1"
local file_path="${2:-}"
if [ -z "$name" ]; then
echo "Usage: gitnexus-context <symbol_name> [file_path]"
echo "Get a 360-degree view of a code symbol: callers, callees, processes, file location."
echo ""
echo "Examples:"
echo ' gitnexus-context "validateUser"'
echo ' gitnexus-context "AuthService" "src/auth/service.py"'
return 1
fi
local args="{\"name\": \"$name\""
[ -n "$file_path" ] && args="$args, \"file_path\": \"$file_path\""
args="$args}"
_gitnexus_call context "$args"
}
gitnexus-impact() {
local target="$1"
local direction="${2:-upstream}"
if [ -z "$target" ]; then
echo "Usage: gitnexus-impact <symbol_name> [upstream|downstream]"
echo "Analyze the blast radius of changing a code symbol."
echo ""
echo " upstream = what depends on this (what breaks if you change it)"
echo " downstream = what this depends on (what it uses)"
echo ""
echo "Examples:"
echo ' gitnexus-impact "AuthService" upstream'
echo ' gitnexus-impact "validateUser" downstream'
return 1
fi
_gitnexus_call impact "{\"target\": \"$target\", \"direction\": \"$direction\"}"
}
gitnexus-cypher() {
local query="$1"
if [ -z "$query" ]; then
echo "Usage: gitnexus-cypher <cypher_query>"
echo "Execute a raw Cypher query against the code knowledge graph."
echo ""
echo "Schema: Nodes: File, Function, Class, Method, Interface, Community, Process"
echo "Edges via CodeRelation.type: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS"
echo ""
echo "Examples:"
echo " gitnexus-cypher 'MATCH (a)-[:CodeRelation {type: \"CALLS\"}]->(b:Function {name: \"save\"}) RETURN a.name, a.filePath'"
echo " gitnexus-cypher 'MATCH (n:Class) RETURN n.name, n.filePath LIMIT 20'"
return 1
fi
_gitnexus_call cypher "{\"query\": \"$query\"}"
}
gitnexus-overview() {
echo "=== Code Knowledge Graph Overview ==="
_gitnexus_call list_repos '{}'
}
# Export functions so they're available in subshells
export -f _gitnexus_call 2>/dev/null
export -f gitnexus-query 2>/dev/null
export -f gitnexus-context 2>/dev/null
export -f gitnexus-impact 2>/dev/null
export -f gitnexus-cypher 2>/dev/null
export -f gitnexus-overview 2>/dev/null

336
eval/bridge/mcp_bridge.py Normal file
View file

@ -0,0 +1,336 @@
"""
MCP Bridge for GitNexus
Starts the GitNexus MCP server as a subprocess and provides a Python interface
to call MCP tools. Used by the bash wrapper scripts and the augmentation layer.
The bridge communicates with the MCP server via stdio using the JSON-RPC protocol.
"""
import json
import logging
import os
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Any
logger = logging.getLogger("mcp_bridge")
class MCPBridge:
"""
Manages a GitNexus MCP server subprocess and proxies tool calls to it.
Usage:
bridge = MCPBridge(repo_path="/path/to/repo")
bridge.start()
result = bridge.call_tool("query", {"query": "authentication"})
bridge.stop()
"""
def __init__(self, repo_path: str | None = None):
self.repo_path = repo_path or os.getcwd()
self.process: subprocess.Popen | None = None
self._request_id = 0
self._lock = threading.Lock()
self._started = False
def start(self) -> bool:
"""Start the GitNexus MCP server subprocess."""
if self._started:
return True
try:
# Find gitnexus binary
gitnexus_bin = self._find_gitnexus()
if not gitnexus_bin:
logger.error("GitNexus not found. Install with: npm install -g gitnexus")
return False
self.process = subprocess.Popen(
[gitnexus_bin, "mcp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.repo_path,
text=False,
)
# Send initialize request
init_result = self._send_request("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "gitnexus-eval", "version": "0.1.0"},
})
if init_result is None:
logger.error("MCP server failed to initialize")
self.stop()
return False
# Send initialized notification
self._send_notification("notifications/initialized", {})
self._started = True
logger.info("MCP bridge started successfully")
return True
except Exception as e:
logger.error(f"Failed to start MCP bridge: {e}")
self.stop()
return False
def stop(self):
"""Stop the MCP server subprocess."""
if self.process:
try:
self.process.stdin.close()
self.process.terminate()
self.process.wait(timeout=5)
except Exception:
try:
self.process.kill()
except Exception:
pass
self.process = None
self._started = False
def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any] | None:
"""
Call a GitNexus MCP tool and return the result.
Returns the tool result content or None on error.
"""
if not self._started:
logger.error("MCP bridge not started")
return None
result = self._send_request("tools/call", {
"name": tool_name,
"arguments": arguments or {},
})
if result is None:
return None
# Extract text content from MCP response
content = result.get("content", [])
if content and isinstance(content, list):
texts = [item.get("text", "") for item in content if item.get("type") == "text"]
return {"text": "\n".join(texts), "raw": content}
return {"text": "", "raw": content}
def list_tools(self) -> list[dict]:
"""List available MCP tools."""
result = self._send_request("tools/list", {})
if result:
return result.get("tools", [])
return []
def read_resource(self, uri: str) -> str | None:
"""Read an MCP resource by URI."""
result = self._send_request("resources/read", {"uri": uri})
if result:
contents = result.get("contents", [])
if contents:
return contents[0].get("text", "")
return None
def _find_gitnexus(self) -> str | None:
"""Find the gitnexus CLI binary."""
# Check if npx is available (preferred - uses local install)
for cmd in ["npx"]:
try:
result = subprocess.run(
[cmd, "gitnexus", "--version"],
capture_output=True, text=True, timeout=15,
cwd=self.repo_path,
)
if result.returncode == 0:
return cmd # Will use "npx gitnexus mcp"
except Exception:
continue
# Check for global install
try:
result = subprocess.run(
["gitnexus", "--version"],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
return "gitnexus"
except Exception:
pass
return None
def _next_id(self) -> int:
with self._lock:
self._request_id += 1
return self._request_id
def _send_request(self, method: str, params: dict) -> dict | None:
"""Send a JSON-RPC request and wait for response."""
if not self.process or not self.process.stdin or not self.process.stdout:
return None
request_id = self._next_id()
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": params,
}
try:
message = json.dumps(request)
# MCP uses Content-Length header framing
header = f"Content-Length: {len(message.encode('utf-8'))}\r\n\r\n"
self.process.stdin.write(header.encode("utf-8"))
self.process.stdin.write(message.encode("utf-8"))
self.process.stdin.flush()
# Read response
response = self._read_response(timeout=30)
if response and response.get("id") == request_id:
if "error" in response:
logger.error(f"MCP error: {response['error']}")
return None
return response.get("result")
return None
except Exception as e:
logger.error(f"MCP request failed: {e}")
return None
def _send_notification(self, method: str, params: dict):
"""Send a JSON-RPC notification (no response expected)."""
if not self.process or not self.process.stdin:
return
notification = {
"jsonrpc": "2.0",
"method": method,
"params": params,
}
try:
message = json.dumps(notification)
header = f"Content-Length: {len(message.encode('utf-8'))}\r\n\r\n"
self.process.stdin.write(header.encode("utf-8"))
self.process.stdin.write(message.encode("utf-8"))
self.process.stdin.flush()
except Exception as e:
logger.error(f"MCP notification failed: {e}")
def _read_response(self, timeout: float = 30) -> dict | None:
"""Read a JSON-RPC response from the MCP server."""
if not self.process or not self.process.stdout:
return None
start = time.time()
try:
while time.time() - start < timeout:
# Read Content-Length header
header_line = b""
while True:
byte = self.process.stdout.read(1)
if not byte:
return None
header_line += byte
if header_line.endswith(b"\r\n\r\n"):
break
if header_line.endswith(b"\n\n"):
break
# Parse content length
header_str = header_line.decode("utf-8").strip()
content_length = None
for line in header_str.split("\r\n"):
if line.lower().startswith("content-length:"):
content_length = int(line.split(":")[1].strip())
break
if content_length is None:
continue
# Read body
body = self.process.stdout.read(content_length)
if not body:
return None
message = json.loads(body.decode("utf-8"))
# Skip notifications (no id), return responses
if "id" in message:
return message
return None
except Exception as e:
logger.error(f"Error reading MCP response: {e}")
return None
class MCPToolCLI:
"""
CLI wrapper that exposes MCP tools as simple command-line calls.
Used by the bash wrapper scripts inside Docker containers.
Usage from bash:
python -m bridge.mcp_bridge query '{"query": "authentication"}'
python -m bridge.mcp_bridge context '{"name": "validateUser"}'
"""
def __init__(self):
self.bridge = MCPBridge()
def run(self, tool_name: str, args_json: str = "{}") -> int:
"""Run a single tool call and print the result."""
try:
args = json.loads(args_json)
except json.JSONDecodeError:
# Try to parse as simple key=value pairs
args = self._parse_simple_args(args_json)
if not self.bridge.start():
print("ERROR: Failed to start GitNexus MCP bridge", file=sys.stderr)
return 1
try:
result = self.bridge.call_tool(tool_name, args)
if result:
print(result.get("text", ""))
return 0
else:
print("No results", file=sys.stderr)
return 1
finally:
self.bridge.stop()
@staticmethod
def _parse_simple_args(args_str: str) -> dict:
"""Parse 'key=value key2=value2' style arguments."""
args = {}
for part in args_str.split():
if "=" in part:
key, value = part.split("=", 1)
args[key] = value
return args
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python -m bridge.mcp_bridge <tool_name> [args_json]", file=sys.stderr)
print("Tools: query, context, impact, cypher, list_repos, detect_changes, rename", file=sys.stderr)
sys.exit(1)
tool = sys.argv[1]
args_json = sys.argv[2] if len(sys.argv) > 2 else "{}"
cli = MCPToolCLI()
sys.exit(cli.run(tool, args_json))

View file

@ -0,0 +1,9 @@
# Claude 3.5 Haiku — fast, cheap, good baseline
# Via OpenRouter (set OPENROUTER_API_KEY in .env)
# To use Anthropic directly, change to: anthropic/claude-3-5-haiku-20241022
model:
model_name: "openrouter/anthropic/claude-3.5-haiku"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 8192
temperature: 0

View file

@ -0,0 +1,9 @@
# Claude Opus 4 — most capable, highest cost
# Via OpenRouter (set OPENROUTER_API_KEY in .env)
# To use Anthropic directly, change to: anthropic/claude-opus-4-20250514
model:
model_name: "openrouter/anthropic/claude-opus-4"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 16384
temperature: 0

View file

@ -0,0 +1,9 @@
# Claude Sonnet 4 — strong all-around model
# Via OpenRouter (set OPENROUTER_API_KEY in .env)
# To use Anthropic directly, change to: anthropic/claude-sonnet-4-20250514
model:
model_name: "openrouter/anthropic/claude-sonnet-4"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 16384
temperature: 0

View file

@ -0,0 +1,7 @@
# GLM 4.7 — via OpenRouter (set OPENROUTER_API_KEY in .env)
model:
model_name: "openrouter/zhipuai/glm-4.7"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 8192
temperature: 0

View file

@ -0,0 +1,7 @@
# GLM 5 — via OpenRouter (set OPENROUTER_API_KEY in .env)
model:
model_name: "openrouter/zhipuai/glm-5"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 8192
temperature: 0

View file

@ -0,0 +1,7 @@
# MiniMax M1 2.5 — via OpenRouter (set OPENROUTER_API_KEY in .env)
model:
model_name: "openrouter/minimax/minimax-m1-2.5"
cost_tracking: "ignore_errors"
model_kwargs:
max_tokens: 8192
temperature: 0

View file

@ -0,0 +1,9 @@
# Baseline mode — no GitNexus, pure mini-swe-agent (control group)
agent:
agent_class: "eval.agents.gitnexus_agent.GitNexusAgent"
gitnexus_mode: "baseline"
step_limit: 30
cost_limit: 3.0
environment:
environment_class: "docker"

View file

@ -0,0 +1,19 @@
# Native mode — GitNexus tools only, no grep enrichment
#
# Explicit tools: gitnexus-query, gitnexus-context, gitnexus-impact, gitnexus-cypher
# Available as fast bash commands (~100ms via eval-server)
#
# Use this mode to isolate the value of explicit tools without grep augmentation.
agent:
agent_class: "eval.agents.gitnexus_agent.GitNexusAgent"
gitnexus_mode: "native"
step_limit: 30
cost_limit: 3.0
track_gitnexus_usage: true
environment:
environment_class: "eval.environments.gitnexus_docker.GitNexusDockerEnvironment"
enable_gitnexus: true
skip_embeddings: true
gitnexus_timeout: 120
eval_server_port: 4848

View file

@ -0,0 +1,24 @@
# Native + Augment mode — the primary evaluation mode
#
# Combines two capabilities (mirroring the Claude Code model):
# 1. Explicit GitNexus tools: gitnexus-query, gitnexus-context, gitnexus-impact, gitnexus-cypher
# Available as fast bash commands (~100ms via eval-server)
# 2. Automatic grep enrichment: grep/rg results are transparently augmented with
# [GitNexus] annotations showing callers, callees, and execution flows
#
# The agent decides when to use explicit tools vs rely on enriched grep results.
agent:
agent_class: "eval.agents.gitnexus_agent.GitNexusAgent"
gitnexus_mode: "native_augment"
step_limit: 30
cost_limit: 3.0
augment_timeout: 5.0
augment_min_pattern_length: 3
track_gitnexus_usage: true
environment:
environment_class: "eval.environments.gitnexus_docker.GitNexusDockerEnvironment"
enable_gitnexus: true
skip_embeddings: true
gitnexus_timeout: 120
eval_server_port: 4848

View file

View file

@ -0,0 +1,397 @@
"""
GitNexus Docker Environment for SWE-bench Evaluation
Extends mini-swe-agent's Docker environment to:
1. Install GitNexus (Node.js + npm + gitnexus package)
2. Run `gitnexus analyze` on the repository
3. Start the eval-server daemon (persistent HTTP server with warm KuzuDB)
4. Install standalone tool scripts in /usr/local/bin/ (works with subprocess.run)
5. Cache indexes per (repo, base_commit) to avoid re-indexing
IMPORTANT: mini-swe-agent runs every command with subprocess.run in a fresh subshell.
This means .bashrc is NOT sourced, exported functions are NOT available, and env vars
don't persist. The tool scripts must be standalone executables in $PATH.
Architecture:
Agent bash cmd /usr/local/bin/gitnexus-query curl localhost:4848/tool/query eval-server KuzuDB
Fallback: npx gitnexus query (cold start, slower)
Tool call latency: ~50-100ms via eval-server, ~5-10s via CLI fallback.
"""
import hashlib
import json
import logging
import shutil
import time
from pathlib import Path
from minisweagent.environments.docker import DockerEnvironment
logger = logging.getLogger("gitnexus_docker")
DEFAULT_CACHE_DIR = Path.home() / ".gitnexus-eval-cache"
EVAL_SERVER_PORT = 4848
# Standalone tool scripts installed into /usr/local/bin/ inside the container.
# Each script calls the eval-server via curl, with a CLI fallback.
# These are standalone — no sourcing, no env inheritance needed.
TOOL_SCRIPT_QUERY = r'''#!/bin/bash
PORT="${GITNEXUS_EVAL_PORT:-__PORT__}"
query="$1"; task_ctx="${2:-}"; goal="${3:-}"
[ -z "$query" ] && echo "Usage: gitnexus-query <query> [task_context] [goal]" && exit 1
args="{\"query\": \"$query\""
[ -n "$task_ctx" ] && args="$args, \"task_context\": \"$task_ctx\""
[ -n "$goal" ] && args="$args, \"goal\": \"$goal\""
args="$args}"
result=$(curl -sf -X POST "http://127.0.0.1:${PORT}/tool/query" -H "Content-Type: application/json" -d "$args" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi
cd /testbed && npx gitnexus query "$query" 2>&1
'''
TOOL_SCRIPT_CONTEXT = r'''#!/bin/bash
PORT="${GITNEXUS_EVAL_PORT:-__PORT__}"
name="$1"; file_path="${2:-}"
[ -z "$name" ] && echo "Usage: gitnexus-context <symbol_name> [file_path]" && exit 1
args="{\"name\": \"$name\""
[ -n "$file_path" ] && args="$args, \"file_path\": \"$file_path\""
args="$args}"
result=$(curl -sf -X POST "http://127.0.0.1:${PORT}/tool/context" -H "Content-Type: application/json" -d "$args" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi
cd /testbed && npx gitnexus context "$name" 2>&1
'''
TOOL_SCRIPT_IMPACT = r'''#!/bin/bash
PORT="${GITNEXUS_EVAL_PORT:-__PORT__}"
target="$1"; direction="${2:-upstream}"
[ -z "$target" ] && echo "Usage: gitnexus-impact <symbol_name> [upstream|downstream]" && exit 1
result=$(curl -sf -X POST "http://127.0.0.1:${PORT}/tool/impact" -H "Content-Type: application/json" -d "{\"target\": \"$target\", \"direction\": \"$direction\"}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi
cd /testbed && npx gitnexus impact "$target" --direction "$direction" 2>&1
'''
TOOL_SCRIPT_CYPHER = r'''#!/bin/bash
PORT="${GITNEXUS_EVAL_PORT:-__PORT__}"
query="$1"
[ -z "$query" ] && echo "Usage: gitnexus-cypher <cypher_query>" && exit 1
result=$(curl -sf -X POST "http://127.0.0.1:${PORT}/tool/cypher" -H "Content-Type: application/json" -d "{\"query\": \"$query\"}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi
cd /testbed && npx gitnexus cypher "$query" 2>&1
'''
TOOL_SCRIPT_AUGMENT = r'''#!/bin/bash
cd /testbed && npx gitnexus augment "$1" 2>&1 || true
'''
TOOL_SCRIPT_OVERVIEW = r'''#!/bin/bash
PORT="${GITNEXUS_EVAL_PORT:-__PORT__}"
echo "=== Code Knowledge Graph Overview ==="
result=$(curl -sf -X POST "http://127.0.0.1:${PORT}/tool/list_repos" -H "Content-Type: application/json" -d "{}" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi
cd /testbed && npx gitnexus list 2>&1
'''
class GitNexusDockerEnvironment(DockerEnvironment):
"""
Docker environment with GitNexus pre-installed, indexed, and eval-server running.
Setup flow:
1. Start Docker container (base SWE-bench image)
2. Install Node.js + gitnexus inside the container
3. Run `gitnexus analyze` (or restore from cache)
4. Start `gitnexus eval-server` daemon (keeps KuzuDB warm)
5. Install standalone tool scripts in /usr/local/bin/
6. Agent runs with near-instant GitNexus tool calls
"""
def __init__(
self,
*,
enable_gitnexus: bool = True,
cache_dir: str | Path | None = None,
skip_embeddings: bool = True,
gitnexus_timeout: int = 120,
eval_server_port: int = EVAL_SERVER_PORT,
**kwargs,
):
super().__init__(**kwargs)
self.enable_gitnexus = enable_gitnexus
self.cache_dir = Path(cache_dir) if cache_dir else DEFAULT_CACHE_DIR
self.skip_embeddings = skip_embeddings
self.gitnexus_timeout = gitnexus_timeout
self.eval_server_port = eval_server_port
self.index_time: float = 0.0
self._gitnexus_ready = False
def start(self) -> dict:
"""Start the container and set up GitNexus."""
result = super().start()
if self.enable_gitnexus:
try:
self._setup_gitnexus()
except Exception as e:
logger.warning(f"GitNexus setup failed, continuing without it: {e}")
self._gitnexus_ready = False
return result
def _setup_gitnexus(self):
"""Install and configure GitNexus in the container."""
start = time.time()
self._ensure_nodejs()
self._install_gitnexus()
self._index_repository()
self._start_eval_server()
self._install_tools()
self.index_time = time.time() - start
self._gitnexus_ready = True
logger.info(f"GitNexus setup completed in {self.index_time:.1f}s")
def _ensure_nodejs(self):
"""Ensure Node.js >= 18 is available in the container."""
check = self.execute({"command": "node --version 2>/dev/null || echo 'NOT_FOUND'"})
output = check.get("output", "").strip()
if "NOT_FOUND" in output:
logger.info("Installing Node.js in container...")
install_cmds = [
"apt-get update -qq",
"apt-get install -y -qq curl ca-certificates",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get install -y -qq nodejs",
]
for cmd in install_cmds:
result = self.execute({"command": cmd, "timeout": 60})
if result.get("returncode", 1) != 0:
raise RuntimeError(f"Failed to install Node.js: {result.get('output', '')}")
else:
logger.info(f"Node.js already available: {output}")
def _install_gitnexus(self):
"""Install the gitnexus npm package globally."""
check = self.execute({"command": "npx gitnexus --version 2>/dev/null || echo 'NOT_FOUND'"})
if "NOT_FOUND" in check.get("output", ""):
logger.info("Installing gitnexus...")
result = self.execute({
"command": "npm install -g gitnexus",
"timeout": 60,
})
if result.get("returncode", 1) != 0:
raise RuntimeError(f"Failed to install gitnexus: {result.get('output', '')}")
def _index_repository(self):
"""Run gitnexus analyze on the repo, using cache if available."""
repo_info = self._get_repo_info()
cache_key = self._make_cache_key(repo_info)
cache_path = self.cache_dir / cache_key
if cache_path.exists():
logger.info(f"Restoring GitNexus index from cache: {cache_key}")
self._restore_cache(cache_path)
return
logger.info("Running gitnexus analyze...")
skip_flag = "--skip-embeddings" if self.skip_embeddings else ""
result = self.execute({
"command": f"cd /testbed && npx gitnexus analyze . {skip_flag} 2>&1",
"timeout": self.gitnexus_timeout,
})
if result.get("returncode", 1) != 0:
output = result.get("output", "")
if "error" in output.lower() and "indexed" not in output.lower():
raise RuntimeError(f"gitnexus analyze failed: {output[-500:]}")
self._save_cache(cache_path, repo_info)
def _start_eval_server(self):
"""Start the GitNexus eval-server daemon in the background."""
logger.info(f"Starting eval-server on port {self.eval_server_port}...")
self.execute({
"command": (
f"nohup npx gitnexus eval-server --port {self.eval_server_port} "
f"--idle-timeout 600 "
f"> /tmp/gitnexus-eval-server.log 2>&1 &"
),
"timeout": 5,
})
# Wait for the server to be ready (up to 15s for KuzuDB init)
for i in range(30):
time.sleep(0.5)
health = self.execute({
"command": f"curl -sf http://127.0.0.1:{self.eval_server_port}/health 2>/dev/null || echo 'NOT_READY'",
"timeout": 3,
})
output = health.get("output", "").strip()
if "NOT_READY" not in output and "ok" in output:
logger.info(f"Eval-server ready after {(i + 1) * 0.5:.1f}s")
return
log_output = self.execute({
"command": "cat /tmp/gitnexus-eval-server.log 2>/dev/null | tail -20",
})
logger.warning(
f"Eval-server didn't become ready in 15s. "
f"Tools will fall back to direct CLI.\n"
f"Server log: {log_output.get('output', 'N/A')}"
)
def _install_tools(self):
"""
Install standalone GitNexus tool scripts in /usr/local/bin/.
Each script is a self-contained bash script that:
1. Calls the eval-server via curl (fast path, ~100ms)
2. Falls back to direct CLI if eval-server is unavailable
These are standalone executables no sourcing, env inheritance, or .bashrc
needed. This is critical because mini-swe-agent runs every command via
subprocess.run in a fresh subshell.
Uses heredocs with quoted delimiter to avoid all quoting/escaping issues.
"""
port = str(self.eval_server_port)
tools = {
"gitnexus-query": TOOL_SCRIPT_QUERY,
"gitnexus-context": TOOL_SCRIPT_CONTEXT,
"gitnexus-impact": TOOL_SCRIPT_IMPACT,
"gitnexus-cypher": TOOL_SCRIPT_CYPHER,
"gitnexus-augment": TOOL_SCRIPT_AUGMENT,
"gitnexus-overview": TOOL_SCRIPT_OVERVIEW,
}
for name, script in tools.items():
script_content = script.replace("__PORT__", port).strip()
# Use heredoc with quoted delimiter — prevents all variable expansion and quoting issues
self.execute({
"command": f"cat << 'GITNEXUS_SCRIPT_EOF' > /usr/local/bin/{name}\n{script_content}\nGITNEXUS_SCRIPT_EOF\nchmod +x /usr/local/bin/{name}",
"timeout": 5,
})
logger.info(f"Installed {len(tools)} GitNexus tool scripts in /usr/local/bin/")
def _get_repo_info(self) -> dict:
"""Get repository identity info from the container."""
repo_result = self.execute({
"command": "cd /testbed && basename $(git remote get-url origin 2>/dev/null || basename $(pwd)) .git"
})
commit_result = self.execute({"command": "cd /testbed && git rev-parse HEAD 2>/dev/null || echo unknown"})
return {
"repo": repo_result.get("output", "unknown").strip(),
"commit": commit_result.get("output", "unknown").strip(),
}
@staticmethod
def _make_cache_key(repo_info: dict) -> str:
"""Create a deterministic cache key from repo info."""
content = f"{repo_info['repo']}:{repo_info['commit']}"
return hashlib.sha256(content.encode()).hexdigest()[:16]
def _save_cache(self, cache_path: Path, repo_info: dict):
"""Save the GitNexus index to the host cache directory."""
try:
cache_path.mkdir(parents=True, exist_ok=True)
find_result = self.execute({
"command": "find /root/.gitnexus -name 'kuzu' -type d 2>/dev/null | head -1"
})
gitnexus_dir = find_result.get("output", "").strip()
if gitnexus_dir:
parent = str(Path(gitnexus_dir).parent)
self.execute({
"command": f"cd {parent} && tar czf /tmp/gitnexus-cache.tar.gz .",
"timeout": 30,
})
container_id = getattr(self, "_container_id", None) or getattr(self, "container_id", None)
if container_id:
import subprocess as sp
sp.run(
["docker", "cp", f"{container_id}:/tmp/gitnexus-cache.tar.gz",
str(cache_path / "index.tar.gz")],
check=True, capture_output=True,
)
(cache_path / "metadata.json").write_text(json.dumps(repo_info, indent=2))
logger.info(f"Cached GitNexus index: {cache_path}")
except Exception as e:
logger.warning(f"Failed to cache GitNexus index: {e}")
if cache_path.exists():
shutil.rmtree(cache_path, ignore_errors=True)
def _restore_cache(self, cache_path: Path):
"""Restore a cached GitNexus index into the container."""
try:
cache_tarball = cache_path / "index.tar.gz"
if not cache_tarball.exists():
logger.warning("Cache tarball not found, re-indexing")
self._index_repository()
return
container_id = getattr(self, "_container_id", None) or getattr(self, "container_id", None)
if container_id:
import subprocess as sp
self.execute({"command": "mkdir -p /root/.gitnexus"})
storage_result = self.execute({
"command": "npx gitnexus list 2>/dev/null | grep -o '/root/.gitnexus/[^ ]*' | head -1 || echo '/root/.gitnexus/repos/default'"
})
storage_path = storage_result.get("output", "").strip() or "/root/.gitnexus/repos/default"
self.execute({"command": f"mkdir -p {storage_path}"})
sp.run(
["docker", "cp", str(cache_tarball), f"{container_id}:/tmp/gitnexus-cache.tar.gz"],
check=True, capture_output=True,
)
self.execute({
"command": f"cd {storage_path} && tar xzf /tmp/gitnexus-cache.tar.gz",
"timeout": 30,
})
logger.info("GitNexus index restored from cache")
except Exception as e:
logger.warning(f"Failed to restore cache, re-indexing: {e}")
self._index_repository()
def stop(self) -> dict:
"""Stop the container, shutting down eval-server first."""
if self._gitnexus_ready:
try:
self.execute({
"command": f"curl -sf -X POST http://127.0.0.1:{self.eval_server_port}/shutdown 2>/dev/null || true",
"timeout": 3,
})
except Exception:
pass
return super().stop()
def get_template_vars(self) -> dict:
"""Add GitNexus-specific template variables."""
base_vars = super().get_template_vars()
base_vars["gitnexus_ready"] = self._gitnexus_ready
base_vars["gitnexus_index_time"] = self.index_time
return base_vars
def serialize(self) -> dict:
"""Include GitNexus environment info in serialization."""
base = super().serialize()
base.setdefault("info", {})["gitnexus_env"] = {
"enabled": self.enable_gitnexus,
"ready": self._gitnexus_ready,
"index_time_seconds": round(self.index_time, 2),
"skip_embeddings": self.skip_embeddings,
"eval_server_port": self.eval_server_port,
}
return base

View file

@ -0,0 +1,80 @@
Please solve this issue: {{task}}
You can execute bash commands and edit files to implement the necessary changes.
## Recommended Workflow
This workflows should be done step-by-step so that you can iterate on your changes and any possible problems.
1. Analyze the codebase by finding and reading relevant files
2. Create a script to reproduce the issue
3. Edit the source code to resolve the issue
4. Verify your fix works by running your script again
5. Test edge cases to ensure your fix is robust
6. Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
Do not combine it with any other command. After this command, you cannot continue working on this task.
## Important Rules
1. Every response must contain exactly one action
2. The action must be enclosed in triple backticks
3. Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
<system_info>
{{system}} {{release}} {{version}} {{machine}}
</system_info>
## Formatting your response
Here is an example of a correct response:
<example_response>
THOUGHT: I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase.
```mswea_bash_command
ls -la
```
</example_response>
## Useful command examples
### Create a new file:
```bash
cat <<'EOF' > newfile.py
import numpy as np
hello = "world"
print(hello)
EOF
```
### Edit files with sed:
{%- if system == "Darwin" -%}
<note>
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
</note>
{%- endif -%}
```bash
# Replace all occurrences
sed -i 's/old_string/new_string/g' filename.py
# Replace only first occurrence
sed -i 's/old_string/new_string/' filename.py
# Replace all occurrences in lines 1-10
sed -i '1,10s/old_string/new_string/g' filename.py
```
### View file content:
```bash
# View specific lines with numbers
nl -ba filename.py | sed -n '10,20p'
```
### Any other command you want to run
```bash
anything
```

View file

@ -0,0 +1,102 @@
Please solve this issue: {{task}}
You can execute bash commands and edit files to implement the necessary changes.
## Recommended Workflow
Work step-by-step so you can iterate on your changes and catch problems early.
1. **Understand the issue** — read the problem statement, identify the symptom and affected area
2. **Find the relevant code** — use `gitnexus-query "<feature area>"` to find execution flows, or `grep` for specific strings
3. **Understand the suspect** — use `gitnexus-context "<symbol>"` to see all callers and callees, then `cat` to read the source
4. **Check blast radius** — before editing shared code, run `gitnexus-impact "<symbol>" upstream` to see what depends on it
5. **Implement the fix** — make minimal, targeted changes
6. **Verify** — run relevant tests, check edge cases
7. **Submit** — issue: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`
Do not combine it with any other command. After this command, you cannot continue working on this task.
## Debugging Patterns
| Symptom | Approach |
|---------|----------|
| Error message / exception | `gitnexus-query` for error text → `gitnexus-context` on throw sites |
| Wrong return value | `gitnexus-context` on the function → trace callees for data flow |
| Missing feature / incomplete behavior | `gitnexus-query` for feature area → find the execution flow → locate the gap |
| Need to understand callers | `gitnexus-context` — graph-complete, finds callers grep would miss |
## Risk Assessment
Before editing shared code, check the blast radius:
| Impact | Risk | Action |
|--------|------|--------|
| <5 symbols at d=1 | Low | Fix with confidence |
| 5-15 symbols at d=1 | Medium | Fix carefully, run broader tests |
| >15 symbols at d=1 | High | Minimal change, run full test suite |
## Important Rules
1. Every response must contain exactly one action
2. The action must be enclosed in triple backticks
3. Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
4. Make minimal, targeted changes. Don't refactor unrelated code.
5. GitNexus tools are ~100ms. Use them when they save you multiple grep iterations.
<system_info>
{{system}} {{release}} {{version}} {{machine}}
</system_info>
## Formatting your response
Here is an example of a correct response:
<example_response>
THOUGHT: The issue mentions a problem with form field validation. Let me search the code knowledge graph for the relevant execution flows to understand how validation works in this codebase.
```mswea_bash_command
gitnexus-query "form field validation"
```
</example_response>
## Useful command examples
### Create a new file:
```bash
cat <<'EOF' > newfile.py
import numpy as np
hello = "world"
print(hello)
EOF
```
### Edit files with sed:
{%- if system == "Darwin" -%}
<note>
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
</note>
{%- endif -%}
```bash
# Replace all occurrences
sed -i 's/old_string/new_string/g' filename.py
# Replace only first occurrence
sed -i 's/old_string/new_string/' filename.py
# Replace all occurrences in lines 1-10
sed -i '1,10s/old_string/new_string/g' filename.py
```
### View file content:
```bash
# View specific lines with numbers
nl -ba filename.py | sed -n '10,20p'
```
### Any other command you want to run
```bash
anything
```

View file

@ -0,0 +1,103 @@
Please solve this issue: {{task}}
You can execute bash commands and edit files to implement the necessary changes.
## Recommended Workflow
Work step-by-step so you can iterate on your changes and catch problems early.
1. **Understand the issue** — read the problem statement, identify the symptom and affected area
2. **Find the relevant code** — use `gitnexus-query "<feature area>"` to find execution flows, or `grep` for specific strings
3. **Understand the suspect** — use `gitnexus-context "<symbol>"` to see all callers and callees, then `cat` to read the source
4. **Check blast radius** — before editing shared code, run `gitnexus-impact "<symbol>" upstream` to see what depends on it
5. **Implement the fix** — make minimal, targeted changes
6. **Verify** — run relevant tests, check edge cases
7. **Submit** — issue: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`
Do not combine it with any other command. After this command, you cannot continue working on this task.
## Debugging Patterns
| Symptom | Approach |
|---------|----------|
| Error message / exception | `gitnexus-query` for error text → `gitnexus-context` on throw sites |
| Wrong return value | `gitnexus-context` on the function → trace callees for data flow |
| Missing feature / incomplete behavior | `gitnexus-query` for feature area → find the execution flow → locate the gap |
| Need to understand callers | `gitnexus-context` — graph-complete, finds callers grep would miss |
## Risk Assessment
Before editing shared code, check the blast radius:
| Impact | Risk | Action |
|--------|------|--------|
| <5 symbols at d=1 | Low | Fix with confidence |
| 5-15 symbols at d=1 | Medium | Fix carefully, run broader tests |
| >15 symbols at d=1 | High | Minimal change, run full test suite |
## Important Rules
1. Every response must contain exactly one action
2. The action must be enclosed in triple backticks
3. Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
4. Make minimal, targeted changes. Don't refactor unrelated code.
5. GitNexus tools are ~100ms. Use them when they save you multiple grep iterations.
6. When grep results show `[GitNexus]` enrichments, use those for navigation.
<system_info>
{{system}} {{release}} {{version}} {{machine}}
</system_info>
## Formatting your response
Here is an example of a correct response:
<example_response>
THOUGHT: The issue mentions a problem with form field validation. Let me search the code knowledge graph for the relevant execution flows to understand how validation works in this codebase.
```mswea_bash_command
gitnexus-query "form field validation"
```
</example_response>
## Useful command examples
### Create a new file:
```bash
cat <<'EOF' > newfile.py
import numpy as np
hello = "world"
print(hello)
EOF
```
### Edit files with sed:
{%- if system == "Darwin" -%}
<note>
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
</note>
{%- endif -%}
```bash
# Replace all occurrences
sed -i 's/old_string/new_string/g' filename.py
# Replace only first occurrence
sed -i 's/old_string/new_string/' filename.py
# Replace all occurrences in lines 1-10
sed -i '1,10s/old_string/new_string/g' filename.py
```
### View file content:
```bash
# View specific lines with numbers
nl -ba filename.py | sed -n '10,20p'
```
### Any other command you want to run
```bash
anything
```

View file

@ -0,0 +1,15 @@
You are a helpful assistant that can interact with a computer to solve software engineering tasks.
Your response must contain exactly ONE bash code block with ONE command (or commands connected with && or ||).
Include a THOUGHT section before your command where you explain your reasoning process.
Format your response as shown in.
<example_response>
Your reasoning and analysis here. Explain why you want to perform the action.
```mswea_bash_command
your_command_here
```
</example_response>
Failure to follow these rules will cause your response to be rejected.

View file

@ -0,0 +1,54 @@
You are a helpful assistant that can interact with a computer to solve software engineering tasks.
Your response must contain exactly ONE bash code block with ONE command (or commands connected with && or ||).
Include a THOUGHT section before your command where you explain your reasoning process.
Format your response as shown in.
<example_response>
Your reasoning and analysis here. Explain why you want to perform the action.
```mswea_bash_command
your_command_here
```
</example_response>
Failure to follow these rules will cause your response to be rejected.
## Code Intelligence
You have **GitNexus** — a knowledge graph over this entire codebase. It knows every function call chain, class hierarchy, execution flow, and symbol relationship. These are fast bash commands (~100ms). Use them when useful, skip them when a simple grep suffices.
### GitNexus Commands
**gitnexus-query "<concept>"** — Find execution flows related to a concept.
Returns ranked execution flow traces with participating symbols and file locations.
```bash
gitnexus-query "form field validation"
```
**gitnexus-context "<symbol>" ["<file_path>"]** — 360-degree view of a symbol.
Returns ALL callers, ALL callees, and execution flows. Graph-complete — finds callers that grep misses.
```bash
gitnexus-context "BoundField" "django/forms/boundfield.py"
```
**gitnexus-impact "<symbol>" [upstream|downstream]** — Blast radius analysis.
What breaks if you change this: d=1 WILL BREAK, d=2 LIKELY AFFECTED, d=3 MAY NEED TESTING.
```bash
gitnexus-impact "BoundField" upstream
```
**gitnexus-cypher "<query>"** — Raw Cypher query against the code graph.
```bash
gitnexus-cypher 'MATCH (a)-[:CodeRelation {type: "CALLS"}]->(b:Function {name: "clean"}) RETURN a.name, a.filePath'
```
### When to Use What
| I need to... | Use |
|---|---|
| Understand how a feature works end-to-end | `gitnexus-query` |
| Find ALL callers of a function | `gitnexus-context` |
| Know what breaks if I change something | `gitnexus-impact` upstream |
| Find a string literal or error message | `grep` |
| Read source code | `cat` / `nl -ba` |

View file

@ -0,0 +1,56 @@
You are a helpful assistant that can interact with a computer to solve software engineering tasks.
Your response must contain exactly ONE bash code block with ONE command (or commands connected with && or ||).
Include a THOUGHT section before your command where you explain your reasoning process.
Format your response as shown in.
<example_response>
Your reasoning and analysis here. Explain why you want to perform the action.
```mswea_bash_command
your_command_here
```
</example_response>
Failure to follow these rules will cause your response to be rejected.
## Code Intelligence
You have **GitNexus** — a knowledge graph over this entire codebase. It knows every function call chain, class hierarchy, execution flow, and symbol relationship. These are fast bash commands (~100ms). Use them when useful, skip them when a simple grep suffices.
Your `grep` results are also automatically enriched with `[GitNexus]` annotations showing callers, callees, and execution flows for matched symbols. Pay attention to these — they often point you to the right code without extra tool calls.
### GitNexus Commands
**gitnexus-query "<concept>"** — Find execution flows related to a concept.
Returns ranked execution flow traces with participating symbols and file locations.
```bash
gitnexus-query "form field validation"
```
**gitnexus-context "<symbol>" ["<file_path>"]** — 360-degree view of a symbol.
Returns ALL callers, ALL callees, and execution flows. Graph-complete — finds callers that grep misses.
```bash
gitnexus-context "BoundField" "django/forms/boundfield.py"
```
**gitnexus-impact "<symbol>" [upstream|downstream]** — Blast radius analysis.
What breaks if you change this: d=1 WILL BREAK, d=2 LIKELY AFFECTED, d=3 MAY NEED TESTING.
```bash
gitnexus-impact "BoundField" upstream
```
**gitnexus-cypher "<query>"** — Raw Cypher query against the code graph.
```bash
gitnexus-cypher 'MATCH (a)-[:CodeRelation {type: "CALLS"}]->(b:Function {name: "clean"}) RETURN a.name, a.filePath'
```
### When to Use What
| I need to... | Use |
|---|---|
| Understand how a feature works end-to-end | `gitnexus-query` |
| Find ALL callers of a function | `gitnexus-context` |
| Know what breaks if I change something | `gitnexus-impact` upstream |
| Find a string literal or error message | `grep` |
| Read source code | `cat` / `nl -ba` |

35
eval/pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[project]
name = "gitnexus-swebench-eval"
version = "0.1.0"
description = "SWE-bench evaluation harness with GitNexus code intelligence integration"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"mini-swe-agent>=2.0.0",
"litellm>=1.50.0",
"datasets>=3.0.0",
"typer>=0.12.0",
"rich>=13.0.0",
"pyyaml>=6.0",
"pandas>=2.0.0",
"tabulate>=0.9.0",
"python-dotenv>=1.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"ruff>=0.5.0",
]
[project.scripts]
gitnexus-eval = "run_eval:app"
gitnexus-eval-analyze = "analysis.analyze_results:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 120
target-version = "py311"

508
eval/run_eval.py Normal file
View file

@ -0,0 +1,508 @@
#!/usr/bin/env python3
"""
GitNexus SWE-bench Evaluation Runner
Main entry point for running SWE-bench evaluations with and without GitNexus.
Supports running a single configuration or a full matrix of models x modes.
Usage:
# Single run (default: native_augment mode — GitNexus tools + grep enrichment)
python run_eval.py single -m claude-sonnet --subset lite --slice 0:5
# Baseline comparison (no GitNexus)
python run_eval.py single -m claude-sonnet --mode baseline --subset lite --slice 0:5
# Matrix run (all models x all modes)
python run_eval.py matrix --subset lite --slice 0:50 --workers 4
# Single instance for debugging
python run_eval.py debug -m claude-haiku -i django__django-16527
"""
import concurrent.futures
import json
import logging
import os
import threading
import time
import traceback
from itertools import product
from pathlib import Path
from typing import Any
import typer
import yaml
from rich.console import Console
from rich.live import Live
from rich.table import Table
# Load .env file from eval/ directory
_env_file = Path(__file__).parent / ".env"
if _env_file.exists():
for line in _env_file.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
key, value = key.strip(), value.strip()
if value and key not in os.environ: # Don't override existing env vars
os.environ[key] = value
logger = logging.getLogger("gitnexus_eval")
console = Console()
app = typer.Typer(rich_markup_mode="rich", add_completion=False)
# Directory paths
EVAL_DIR = Path(__file__).parent
CONFIGS_DIR = EVAL_DIR / "configs"
MODELS_DIR = CONFIGS_DIR / "models"
MODES_DIR = CONFIGS_DIR / "modes"
DEFAULT_OUTPUT_DIR = EVAL_DIR / "results"
# Available models and modes (discovered from config files)
AVAILABLE_MODELS = sorted([p.stem for p in MODELS_DIR.glob("*.yaml")])
AVAILABLE_MODES = sorted([p.stem for p in MODES_DIR.glob("*.yaml")])
# SWE-bench dataset mapping (same as mini-swe-agent)
DATASET_MAPPING = {
"full": "princeton-nlp/SWE-Bench",
"verified": "princeton-nlp/SWE-Bench_Verified",
"lite": "princeton-nlp/SWE-Bench_Lite",
}
_output_lock = threading.Lock()
def load_yaml_config(path: Path) -> dict:
"""Load a YAML config file."""
with open(path) as f:
return yaml.safe_load(f) or {}
def merge_configs(*configs: dict) -> dict:
"""Recursively merge multiple config dicts (later values win)."""
result = {}
for config in configs:
for key, value in config.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = merge_configs(result[key], value)
else:
result[key] = value
return result
def build_config(model_name: str, mode_name: str) -> dict:
"""Build a complete config from model + mode YAML files."""
model_file = MODELS_DIR / f"{model_name}.yaml"
mode_file = MODES_DIR / f"{mode_name}.yaml"
if not model_file.exists():
raise FileNotFoundError(f"Model config not found: {model_file}")
if not mode_file.exists():
raise FileNotFoundError(f"Mode config not found: {mode_file}")
model_config = load_yaml_config(model_file)
mode_config = load_yaml_config(mode_file)
return merge_configs(mode_config, model_config)
def load_instances(subset: str, split: str, slice_spec: str = "", filter_spec: str = "") -> list[dict]:
"""Load SWE-bench instances."""
from datasets import load_dataset
import re
dataset_path = DATASET_MAPPING.get(subset, subset)
logger.info(f"Loading dataset: {dataset_path}, split: {split}")
instances = list(load_dataset(dataset_path, split=split))
if filter_spec:
instances = [i for i in instances if re.match(filter_spec, i["instance_id"])]
if slice_spec:
values = [int(x) if x else None for x in slice_spec.split(":")]
instances = instances[slice(*values)]
logger.info(f"Loaded {len(instances)} instances")
return instances
def get_swebench_docker_image(instance: dict) -> str:
"""Get Docker image name for a SWE-bench instance."""
image_name = instance.get("image_name")
if image_name is None:
iid = instance["instance_id"]
id_docker = iid.replace("__", "_1776_")
image_name = f"docker.io/swebench/sweb.eval.x86_64.{id_docker}:latest".lower()
return image_name
def process_instance(
instance: dict,
config: dict,
output_dir: Path,
model_name: str,
mode_name: str,
) -> dict:
"""
Process a single SWE-bench instance with the given config.
Returns result dict with instance_id, exit_status, submission, metrics.
"""
from minisweagent.models import get_model
instance_id = instance["instance_id"]
run_id = f"{model_name}_{mode_name}"
instance_dir = output_dir / run_id / instance_id
instance_dir.mkdir(parents=True, exist_ok=True)
result = {
"instance_id": instance_id,
"model": model_name,
"mode": mode_name,
"exit_status": None,
"submission": "",
"cost": 0.0,
"n_calls": 0,
"gitnexus_metrics": {},
}
agent = None
try:
# Build model
model = get_model(config=config.get("model", {}))
# Build environment
env_config = dict(config.get("environment", {}))
env_class_name = env_config.pop("environment_class", "docker")
if env_class_name == "eval.environments.gitnexus_docker.GitNexusDockerEnvironment":
from eval.environments.gitnexus_docker import GitNexusDockerEnvironment
env_config["image"] = get_swebench_docker_image(instance)
env = GitNexusDockerEnvironment(**env_config)
else:
from minisweagent.environments.docker import DockerEnvironment
env = DockerEnvironment(image=get_swebench_docker_image(instance), **env_config)
# Build agent
agent_config = dict(config.get("agent", {}))
agent_class_name = agent_config.pop("agent_class", "eval.agents.gitnexus_agent.GitNexusAgent")
from eval.agents.gitnexus_agent import GitNexusAgent
traj_path = instance_dir / f"{instance_id}.traj.json"
agent_config["output_path"] = traj_path
agent = GitNexusAgent(model, env, **agent_config)
# Run
logger.info(f"[{run_id}] Starting {instance_id}")
info = agent.run(instance["problem_statement"])
result["exit_status"] = info.get("exit_status")
result["submission"] = info.get("submission", "")
result["cost"] = agent.cost
result["n_calls"] = agent.n_calls
result["gitnexus_metrics"] = agent.gitnexus_metrics.to_dict()
except Exception as e:
logger.error(f"[{run_id}] Error on {instance_id}: {e}")
result["exit_status"] = type(e).__name__
result["error"] = str(e)
result["traceback"] = traceback.format_exc()
finally:
if agent:
agent.save(
instance_dir / f"{instance_id}.traj.json",
{"instance_id": instance_id, "run_id": run_id},
)
# Update predictions file
_update_preds(output_dir / run_id / "preds.json", instance_id, model_name, result)
return result
def _update_preds(preds_path: Path, instance_id: str, model_name: str, result: dict):
"""Thread-safe update of predictions file."""
with _output_lock:
preds_path.parent.mkdir(parents=True, exist_ok=True)
data = {}
if preds_path.exists():
data = json.loads(preds_path.read_text())
data[instance_id] = {
"model_name_or_path": model_name,
"instance_id": instance_id,
"model_patch": result.get("submission", ""),
}
preds_path.write_text(json.dumps(data, indent=2))
def run_configuration(
model_name: str,
mode_name: str,
instances: list[dict],
output_dir: Path,
workers: int = 1,
redo_existing: bool = False,
) -> list[dict]:
"""Run a single (model, mode) configuration across all instances."""
config = build_config(model_name, mode_name)
run_id = f"{model_name}_{mode_name}"
run_dir = output_dir / run_id
# Skip existing instances
if not redo_existing and (run_dir / "preds.json").exists():
existing = set(json.loads((run_dir / "preds.json").read_text()).keys())
instances = [i for i in instances if i["instance_id"] not in existing]
if not instances:
logger.info(f"[{run_id}] All instances already completed, skipping")
return []
console.print(f" [bold]{run_id}[/bold]: {len(instances)} instances, {workers} workers")
results = []
if workers <= 1:
for instance in instances:
result = process_instance(instance, config, output_dir, model_name, mode_name)
results.append(result)
else:
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = {
executor.submit(
process_instance, instance, config, output_dir, model_name, mode_name
): instance["instance_id"]
for instance in instances
}
for future in concurrent.futures.as_completed(futures):
try:
results.append(future.result())
except Exception as e:
iid = futures[future]
logger.error(f"[{run_id}] Uncaught error for {iid}: {e}")
# Save run summary
summary = {
"run_id": run_id,
"model": model_name,
"mode": mode_name,
"config": config,
"total_instances": len(results),
"completed": sum(1 for r in results if r["exit_status"] not in [None, "error"]),
"total_cost": sum(r.get("cost", 0) for r in results),
"total_api_calls": sum(r.get("n_calls", 0) for r in results),
"results": results,
}
(run_dir / "summary.json").mkdir(parents=True, exist_ok=True) if not run_dir.exists() else None
run_dir.mkdir(parents=True, exist_ok=True)
(run_dir / "summary.json").write_text(json.dumps(summary, indent=2, default=str))
return results
# ─── CLI Commands ───────────────────────────────────────────────────────────
@app.command()
def single(
model: str = typer.Option(..., "-m", "--model", help=f"Model config name. Available: {', '.join(AVAILABLE_MODELS)}"),
mode: str = typer.Option("native_augment", "--mode", help=f"Evaluation mode. Available: {', '.join(AVAILABLE_MODES)}"),
subset: str = typer.Option("lite", "--subset", help="SWE-bench subset: lite, verified, full"),
split: str = typer.Option("dev", "--split", help="Dataset split"),
slice_spec: str = typer.Option("", "--slice", help="Slice spec (e.g., '0:5')"),
filter_spec: str = typer.Option("", "--filter", help="Filter instance IDs by regex"),
workers: int = typer.Option(1, "-w", "--workers", help="Parallel workers"),
output: str = typer.Option(str(DEFAULT_OUTPUT_DIR), "-o", "--output", help="Output directory"),
redo: bool = typer.Option(False, "--redo", help="Redo existing instances"),
):
"""Run a single (model, mode) configuration on SWE-bench."""
output_dir = Path(output)
instances = load_instances(subset, split, slice_spec, filter_spec)
console.print(f"\n[bold]Running evaluation:[/bold] {model} + {mode}")
console.print(f" Instances: {len(instances)}")
console.print(f" Output: {output_dir}\n")
results = run_configuration(model, mode, instances, output_dir, workers, redo)
# Print summary
_print_summary(results, model, mode)
@app.command()
def matrix(
models: list[str] = typer.Option(AVAILABLE_MODELS, "-m", "--models", help="Models to evaluate (comma-separated or repeated)"),
modes: list[str] = typer.Option(AVAILABLE_MODES, "--modes", help="Modes to evaluate"),
subset: str = typer.Option("lite", "--subset", help="SWE-bench subset"),
split: str = typer.Option("dev", "--split", help="Dataset split"),
slice_spec: str = typer.Option("", "--slice", help="Slice spec"),
filter_spec: str = typer.Option("", "--filter", help="Filter instances by regex"),
workers: int = typer.Option(1, "-w", "--workers", help="Parallel workers per config"),
output: str = typer.Option(str(DEFAULT_OUTPUT_DIR), "-o", "--output", help="Output directory"),
redo: bool = typer.Option(False, "--redo", help="Redo existing instances"),
):
"""Run the full evaluation matrix: all models x all modes."""
output_dir = Path(output)
instances = load_instances(subset, split, slice_spec, filter_spec)
combos = list(product(models, modes))
console.print(f"\n[bold]Matrix evaluation:[/bold] {len(models)} models x {len(modes)} modes = {len(combos)} configs")
console.print(f" Models: {', '.join(models)}")
console.print(f" Modes: {', '.join(modes)}")
console.print(f" Instances per config: {len(instances)}")
console.print(f" Total runs: {len(combos) * len(instances)}")
console.print(f" Output: {output_dir}\n")
all_results = {}
for model_name, mode_name in combos:
run_id = f"{model_name}_{mode_name}"
console.print(f"\n[bold cyan]━━━ {run_id} ━━━[/bold cyan]")
results = run_configuration(model_name, mode_name, instances, output_dir, workers, redo)
all_results[run_id] = results
# Print comparative summary
_print_matrix_summary(all_results)
# Save master summary
master = {
"timestamp": time.time(),
"models": models,
"modes": modes,
"subset": subset,
"n_instances": len(instances),
"runs": {
run_id: {
"total": len(results),
"cost": sum(r.get("cost", 0) for r in results),
"api_calls": sum(r.get("n_calls", 0) for r in results),
}
for run_id, results in all_results.items()
},
}
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "matrix_summary.json").write_text(json.dumps(master, indent=2, default=str))
console.print(f"\n[green]Results saved to {output_dir}[/green]")
@app.command()
def debug(
model: str = typer.Option("claude-haiku", "-m", "--model", help="Model config name"),
mode: str = typer.Option("native_augment", "--mode", help="Evaluation mode"),
instance_id: str = typer.Option(..., "-i", "--instance", help="SWE-bench instance ID"),
subset: str = typer.Option("lite", "--subset", help="SWE-bench subset"),
split: str = typer.Option("dev", "--split"),
output: str = typer.Option(str(DEFAULT_OUTPUT_DIR / "debug"), "-o", "--output"),
):
"""Debug a single SWE-bench instance."""
from datasets import load_dataset
dataset_path = DATASET_MAPPING.get(subset, subset)
instances = {inst["instance_id"]: inst for inst in load_dataset(dataset_path, split=split)}
if instance_id not in instances:
console.print(f"[red]Instance '{instance_id}' not found in {subset}/{split}[/red]")
raise typer.Exit(1)
instance = instances[instance_id]
config = build_config(model, mode)
output_dir = Path(output)
console.print(f"\n[bold]Debug run:[/bold] {model} + {mode}")
console.print(f" Instance: {instance_id}")
console.print(f" Problem: {instance['problem_statement'][:200]}...\n")
result = process_instance(instance, config, output_dir, model, mode)
_print_summary([result], model, mode)
@app.command()
def list_configs():
"""List available model and mode configurations."""
console.print("\n[bold]Available Models:[/bold]")
for name in AVAILABLE_MODELS:
config = load_yaml_config(MODELS_DIR / f"{name}.yaml")
model_name = config.get("model", {}).get("model_name", "unknown")
console.print(f" {name:<20} {model_name}")
console.print("\n[bold]Available Modes:[/bold]")
for name in AVAILABLE_MODES:
config = load_yaml_config(MODES_DIR / f"{name}.yaml")
gn_mode = config.get("agent", {}).get("gitnexus_mode", "baseline")
console.print(f" {name:<20} gitnexus_mode={gn_mode}")
console.print(f"\n[bold]Matrix:[/bold] {len(AVAILABLE_MODELS)} models x {len(AVAILABLE_MODES)} modes = {len(AVAILABLE_MODELS) * len(AVAILABLE_MODES)} configurations")
# ─── Summary Output ────────────────────────────────────────────────────────
def _print_summary(results: list[dict], model: str, mode: str):
"""Print a summary table for a single run."""
if not results:
console.print("[yellow]No results to display[/yellow]")
return
table = Table(title=f"{model} + {mode}")
table.add_column("Metric", style="bold")
table.add_column("Value")
total = len(results)
completed = sum(1 for r in results if r.get("submission"))
total_cost = sum(r.get("cost", 0) for r in results)
total_calls = sum(r.get("n_calls", 0) for r in results)
table.add_row("Instances", str(total))
table.add_row("Completed", f"{completed}/{total}")
table.add_row("Total Cost", f"${total_cost:.4f}")
table.add_row("Total API Calls", str(total_calls))
table.add_row("Avg Cost/Instance", f"${total_cost / max(total, 1):.4f}")
table.add_row("Avg Calls/Instance", f"{total_calls / max(total, 1):.1f}")
# GitNexus-specific metrics
gn_tool_calls = sum(
r.get("gitnexus_metrics", {}).get("total_tool_calls", 0) for r in results
)
gn_augment_hits = sum(
r.get("gitnexus_metrics", {}).get("augmentation_hits", 0) for r in results
)
if gn_tool_calls > 0:
table.add_row("GitNexus Tool Calls", str(gn_tool_calls))
if gn_augment_hits > 0:
table.add_row("Augmentation Hits", str(gn_augment_hits))
console.print(table)
def _print_matrix_summary(all_results: dict[str, list[dict]]):
"""Print a comparative matrix summary."""
table = Table(title="Evaluation Matrix Summary")
table.add_column("Configuration", style="bold")
table.add_column("Instances")
table.add_column("Completed")
table.add_column("Cost")
table.add_column("API Calls")
table.add_column("GN Tools")
for run_id, results in sorted(all_results.items()):
total = len(results)
completed = sum(1 for r in results if r.get("submission"))
cost = sum(r.get("cost", 0) for r in results)
calls = sum(r.get("n_calls", 0) for r in results)
gn_calls = sum(r.get("gitnexus_metrics", {}).get("total_tool_calls", 0) for r in results)
table.add_row(
run_id,
str(total),
f"{completed}/{total}",
f"${cost:.2f}",
str(calls),
str(gn_calls) if gn_calls > 0 else "-",
)
console.print(table)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
app()

View file

@ -0,0 +1,10 @@
{
"name": "gitnexus",
"description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase.",
"version": "1.0.0",
"author": {
"name": "GitNexus"
},
"homepage": "https://github.com/nicosxt/gitnexus",
"repository": "https://github.com/nicosxt/gitnexus"
}

View file

@ -0,0 +1,123 @@
#!/usr/bin/env node
/**
* GitNexus Claude Code Hook
*
* PreToolUse handler intercepts Grep/Glob/Bash searches
* and augments with graph context from the GitNexus index.
*
* NOTE: SessionStart hooks are broken on Windows (Claude Code bug).
* Session context is injected via CLAUDE.md / skills instead.
*/
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
/**
* Read JSON input from stdin synchronously.
*/
function readInput() {
try {
const data = fs.readFileSync(0, 'utf-8');
return JSON.parse(data);
} catch {
return {};
}
}
/**
* Check if a directory (or ancestor) has a .gitnexus index.
*/
function findGitNexusIndex(startDir) {
let dir = startDir || process.cwd();
for (let i = 0; i < 5; i++) {
if (fs.existsSync(path.join(dir, '.gitnexus'))) {
return true;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return false;
}
/**
* Extract search pattern from tool input.
*/
function extractPattern(toolName, toolInput) {
if (toolName === 'Grep') {
return toolInput.pattern || null;
}
if (toolName === 'Glob') {
const raw = toolInput.pattern || '';
const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
return match ? match[1] : null;
}
if (toolName === 'Bash') {
const cmd = toolInput.command || '';
if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
const tokens = cmd.split(/\s+/);
let foundCmd = false;
let skipNext = false;
const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']);
for (const token of tokens) {
if (skipNext) { skipNext = false; continue; }
if (!foundCmd) {
if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
continue;
}
if (token.startsWith('-')) {
if (flagsWithValues.has(token)) skipNext = true;
continue;
}
const cleaned = token.replace(/['"]/g, '');
return cleaned.length >= 3 ? cleaned : null;
}
return null;
}
return null;
}
function main() {
try {
const input = readInput();
const hookEvent = input.hook_event_name || '';
if (hookEvent !== 'PreToolUse') return;
const cwd = input.cwd || process.cwd();
if (!findGitNexusIndex(cwd)) return;
const toolName = input.tool_name || '';
const toolInput = input.tool_input || {};
if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
const pattern = extractPattern(toolName, toolInput);
if (!pattern || pattern.length < 3) return;
const result = execFileSync(
'gitnexus',
['augment', pattern],
{ encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
);
if (result && result.trim()) {
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext: result.trim()
}
}));
}
} catch {
// Graceful failure
}
}
main();

View file

@ -0,0 +1,17 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Grep|Glob|Bash",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js",
"timeout": 10,
"statusMessage": "Enriching with GitNexus graph context..."
}
]
}
]
}
}

View file

@ -0,0 +1,78 @@
#!/bin/bash
# GitNexus PreToolUse hook for Claude Code
# Intercepts Grep/Glob/Bash searches and augments with graph context.
# Receives JSON on stdin with { tool_name, tool_input, cwd, ... }
# Returns JSON with additionalContext for graph-enriched results.
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
# Extract search pattern based on tool type
PATTERN=""
case "$TOOL_NAME" in
Grep)
PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
;;
Glob)
# Glob patterns are file paths, not search terms — extract meaningful part
RAW=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
# Strip glob syntax to get the meaningful name (e.g., "**/*.ts" → skip, "auth*.ts" → "auth")
PATTERN=$(echo "$RAW" | sed -n 's/.*[*\/]\([a-zA-Z][a-zA-Z0-9_-]*\).*/\1/p')
;;
Bash)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
# Only augment grep/rg commands
if echo "$CMD" | grep -qE '\brg\b|\bgrep\b'; then
# Extract pattern from rg/grep
if echo "$CMD" | grep -qE '\brg\b'; then
PATTERN=$(echo "$CMD" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
elif echo "$CMD" | grep -qE '\bgrep\b'; then
PATTERN=$(echo "$CMD" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
fi
fi
;;
*)
# Not a search tool — skip
exit 0
;;
esac
# Skip if pattern too short or empty
if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then
exit 0
fi
# Check if we're in a GitNexus-indexed repo
dir="${CWD:-$PWD}"
found=false
for i in 1 2 3 4 5; do
if [ -d "$dir/.gitnexus" ]; then
found=true
break
fi
parent="$(dirname "$dir")"
[ "$parent" = "$dir" ] && break
dir="$parent"
done
if [ "$found" = false ]; then
exit 0
fi
# Run gitnexus augment — must be fast (<500ms target)
RESULT=$(cd "$CWD" && npx -y gitnexus augment "$PATTERN" 2>/dev/null)
if [ -n "$RESULT" ]; then
ESCAPED=$(echo "$RESULT" | jq -Rs .)
jq -n --argjson ctx "$ESCAPED" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: $ctx
}
}'
else
exit 0
fi

View file

@ -0,0 +1,41 @@
// GitNexus SessionStart hook for Claude Code
// Fires on session startup. Stdout is injected into Claude's context.
// Checks if the current directory has a GitNexus index.
const fs = require('fs');
const path = require('path');
let dir = process.cwd();
let found = false;
for (let i = 0; i < 5; i++) {
if (fs.existsSync(path.join(dir, '.gitnexus'))) {
found = true;
break;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
if (!found) {
process.exit(0);
}
process.stdout.write(`## GitNexus Code Intelligence
This codebase is indexed by GitNexus, providing a knowledge graph with execution flows, relationships, and semantic search.
**Available MCP Tools:**
- \`query\` — Process-grouped code intelligence (execution flows related to a concept)
- \`context\` — 360-degree symbol view (categorized refs, process participation)
- \`impact\` — Blast radius analysis (what breaks if you change a symbol)
- \`detect_changes\` — Git-diff impact analysis (what do your changes affect)
- \`rename\` — Multi-file coordinated rename with confidence tags
- \`cypher\` — Raw graph queries
- \`list_repos\` — Discover indexed repos
**Quick Start:** READ \`gitnexus://repo/{name}/context\` for codebase overview, then use \`query\` to find execution flows.
**Resources:** \`gitnexus://repo/{name}/context\` (overview), \`/processes\` (execution flows), \`/schema\` (for Cypher)
`);
process.exit(0);

View file

@ -0,0 +1,42 @@
#!/bin/bash
# GitNexus SessionStart hook for Claude Code
# Fires on session startup. Stdout is injected into Claude's context.
# Checks if the current directory has a GitNexus index.
dir="$PWD"
found=false
for i in 1 2 3 4 5; do
if [ -d "$dir/.gitnexus" ]; then
found=true
break
fi
parent="$(dirname "$dir")"
[ "$parent" = "$dir" ] && break
dir="$parent"
done
if [ "$found" = false ]; then
exit 0
fi
# Inject GitNexus context — this stdout goes directly into Claude's context
cat << 'EOF'
## GitNexus Code Intelligence
This codebase is indexed by GitNexus, providing a knowledge graph with execution flows, relationships, and semantic search.
**Available MCP Tools:**
- `query` — Process-grouped code intelligence (execution flows related to a concept)
- `context` — 360-degree symbol view (categorized refs, process participation)
- `impact` — Blast radius analysis (what breaks if you change a symbol)
- `detect_changes` — Git-diff impact analysis (what do your changes affect)
- `rename` — Multi-file coordinated rename with confidence tags
- `cypher` — Raw graph queries
- `list_repos` — Discover indexed repos
**Quick Start:** READ `gitnexus://repo/{name}/context` for codebase overview, then use `query` to find execution flows.
**Resources:** `gitnexus://repo/{name}/context` (overview), `/processes` (execution flows), `/schema` (for Cypher)
EOF
exit 0

View file

@ -0,0 +1,85 @@
---
name: gitnexus-debugging
description: Trace bugs through call chains using knowledge graph
---
# Debugging with GitNexus
## When to Use
- "Why is this function failing?"
- "Trace where this error comes from"
- "Who calls this method?"
- "This endpoint returns 500"
- Investigating bugs, errors, or unexpected behavior
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
## Debugging Patterns
| Symptom | GitNexus Approach |
|---------|-------------------|
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
| Recent regression | `detect_changes` to see what your changes affect |
## Tools
**gitnexus_query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
```
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
→ Step 3: validatePayment → calls fetchRates (external)
4. Root cause: fetchRates calls external API without proper timeout
```

View file

@ -0,0 +1,75 @@
---
name: gitnexus-exploring
description: Navigate unfamiliar code using GitNexus knowledge graph
---
# Exploring Codebases with GitNexus
## When to Use
- "How does authentication work?"
- "What's the project structure?"
- "Show me the main components"
- "Where is the database logic?"
- Understanding code you haven't seen before
## Workflow
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
## Resources
| Resource | What you get |
|----------|-------------|
| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) |
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) |
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) |
## Tools
**gitnexus_query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
```
## Example: "How does payment processing work?"
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
```

View file

@ -0,0 +1,94 @@
---
name: gitnexus-impact-analysis
description: Analyze blast radius before making code changes
---
# Impact Analysis with GitNexus
## When to Use
- "Is it safe to change this function?"
- "What will break if I modify X?"
- "Show me the blast radius"
- "Who uses this code?"
- Before making non-trivial code changes
- Before committing — to understand what your changes affect
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
## Understanding Output
| Depth | Risk Level | Meaning |
|-------|-----------|---------|
| d=1 | **WILL BREAK** | Direct callers/importers |
| d=2 | LIKELY AFFECTED | Indirect dependencies |
| d=3 | MAY NEED TESTING | Transitive effects |
## Risk Assessment
| Affected | Risk |
|----------|------|
| <5 symbols, few processes | LOW |
| 5-15 symbols, 2-5 processes | MEDIUM |
| >15 symbols or many processes | HIGH |
| Critical path (auth, payments) | CRITICAL |
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
maxDepth: 3
})
→ d=1 (WILL BREAK):
- loginHandler (src/auth/login.ts:42) [CALLS, 100%]
- apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]
→ d=2 (LIKELY AFFECTED):
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
→ Risk: MEDIUM
```
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
2. READ gitnexus://repo/my-app/processes
→ LoginFlow and TokenRefresh touch validateUser
3. Risk: 2 direct callers, 2 processes = MEDIUM
```

View file

@ -0,0 +1,113 @@
---
name: gitnexus-refactoring
description: Plan safe refactors using blast radius and dependency mapping
---
# Refactoring with GitNexus
## When to Use
- "Rename this function safely"
- "Extract this into a module"
- "Split this service"
- "Move this to a new file"
- Any task involving renaming, extracting, splitting, or restructuring code
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
```
## Risk Rules
| Risk Factor | Mitigation |
|-------------|------------|
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

View file

@ -0,0 +1,50 @@
#!/bin/bash
# GitNexus beforeShellExecution hook for Cursor
# Receives JSON on stdin with { command, cwd, timeout }
# Returns JSON on stdout with { permission, agent_message }
#
# Extracts search pattern from grep/rg commands, runs gitnexus augment,
# and injects the enriched context via agent_message.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.command // empty' 2>/dev/null)
if [ -z "$COMMAND" ]; then
echo '{"permission":"allow"}'
exit 0
fi
# Skip non-search commands
case "$COMMAND" in
cd\ *|npm\ *|yarn\ *|pnpm\ *|git\ commit*|git\ push*|git\ pull*|mkdir\ *|rm\ *|cp\ *|mv\ *|echo\ *|cat\ *)
echo '{"permission":"allow"}'
exit 0
;;
esac
# Extract search pattern from rg/grep commands
PATTERN=""
if echo "$COMMAND" | grep -qE '\brg\b'; then
PATTERN=$(echo "$COMMAND" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
elif echo "$COMMAND" | grep -qE '\bgrep\b'; then
PATTERN=$(echo "$COMMAND" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
fi
if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then
echo '{"permission":"allow"}'
exit 0
fi
# Run gitnexus augment
RESULT=$(npx -y gitnexus augment "$PATTERN" 2>/dev/null)
if [ -n "$RESULT" ]; then
# Escape for JSON
ESCAPED=$(echo "$RESULT" | jq -Rs .)
echo "{\"permission\":\"allow\",\"agent_message\":$ESCAPED}"
else
echo '{"permission":"allow"}'
fi
exit 0

View file

@ -0,0 +1,12 @@
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "./hooks/augment-shell.sh",
"timeout": 5,
"matcher": "\\brg\\b|\\bgrep\\b"
}
]
}
}

View file

@ -0,0 +1,85 @@
---
name: gitnexus-debugging
description: Trace bugs through call chains using knowledge graph
---
# Debugging with GitNexus
## When to Use
- "Why is this function failing?"
- "Trace where this error comes from"
- "Who calls this method?"
- "This endpoint returns 500"
- Investigating bugs, errors, or unexpected behavior
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
## Debugging Patterns
| Symptom | GitNexus Approach |
|---------|-------------------|
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
| Recent regression | `detect_changes` to see what your changes affect |
## Tools
**gitnexus_query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
```
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
→ Step 3: validatePayment → calls fetchRates (external)
4. Root cause: fetchRates calls external API without proper timeout
```

View file

@ -0,0 +1,75 @@
---
name: gitnexus-exploring
description: Navigate unfamiliar code using GitNexus knowledge graph
---
# Exploring Codebases with GitNexus
## When to Use
- "How does authentication work?"
- "What's the project structure?"
- "Show me the main components"
- "Where is the database logic?"
- Understanding code you haven't seen before
## Workflow
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
## Resources
| Resource | What you get |
|----------|-------------|
| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) |
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) |
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) |
## Tools
**gitnexus_query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
```
## Example: "How does payment processing work?"
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
```

View file

@ -0,0 +1,94 @@
---
name: gitnexus-impact-analysis
description: Analyze blast radius before making code changes
---
# Impact Analysis with GitNexus
## When to Use
- "Is it safe to change this function?"
- "What will break if I modify X?"
- "Show me the blast radius"
- "Who uses this code?"
- Before making non-trivial code changes
- Before committing — to understand what your changes affect
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
## Understanding Output
| Depth | Risk Level | Meaning |
|-------|-----------|---------|
| d=1 | **WILL BREAK** | Direct callers/importers |
| d=2 | LIKELY AFFECTED | Indirect dependencies |
| d=3 | MAY NEED TESTING | Transitive effects |
## Risk Assessment
| Affected | Risk |
|----------|------|
| <5 symbols, few processes | LOW |
| 5-15 symbols, 2-5 processes | MEDIUM |
| >15 symbols or many processes | HIGH |
| Critical path (auth, payments) | CRITICAL |
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
maxDepth: 3
})
→ d=1 (WILL BREAK):
- loginHandler (src/auth/login.ts:42) [CALLS, 100%]
- apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]
→ d=2 (LIKELY AFFECTED):
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
→ Risk: MEDIUM
```
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
2. READ gitnexus://repo/my-app/processes
→ LoginFlow and TokenRefresh touch validateUser
3. Risk: 2 direct callers, 2 processes = MEDIUM
```

View file

@ -0,0 +1,113 @@
---
name: gitnexus-refactoring
description: Plan safe refactors using blast radius and dependency mapping
---
# Refactoring with GitNexus
## When to Use
- "Rename this function safely"
- "Extract this into a module"
- "Split this service"
- "Move this to a new file"
- Any task involving renaming, extracting, splitting, or restructuring code
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
```
## Risk Rules
| Risk Factor | Mitigation |
|-------------|------------|
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

File diff suppressed because it is too large Load diff

View file

@ -1,47 +0,0 @@
{
"name": "gitnexus-mcp",
"version": "0.2.0",
"description": "MCP server for GitNexus code intelligence - connect Cursor, Claude, and other AI agents to your codebase",
"author": "Abhigyan Patwari",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/abhigyanpatwari/GitNexus"
},
"keywords": [
"mcp",
"model-context-protocol",
"code-intelligence",
"cursor",
"claude",
"ai-agent",
"gitnexus"
],
"type": "module",
"bin": {
"gitnexus-mcp": "./dist/cli.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"dev": "tsx watch src/cli.ts",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"uuid": "^13.0.0",
"ws": "^8.16.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.10",
"tsx": "^4.0.0",
"typescript": "^5.4.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View file

@ -1,36 +0,0 @@
/**
* Bridge Protocol Types
*
* JSON-RPC-like protocol for communication between bridge and browser.
*/
export interface BridgeMessage {
id: string;
type?: 'register_peer' | 'tool_call' | 'tool_result' | 'agent_info' | 'handshake' | 'handshake_ack' | 'context';
method?: string;
params?: any;
result?: any;
error?: {
code?: number;
message: string;
};
agentName?: string;
peerId?: string;
}
export type ToolCallRequest = BridgeMessage & { method: string };
export type ToolCallResponse = BridgeMessage & ({ result: any } | { error: any });
/**
* Check if message is a request (has method)
*/
export function isRequest(msg: BridgeMessage): msg is ToolCallRequest {
return typeof msg.method === 'string';
}
/**
* Check if message is a response (has result or error)
*/
export function isResponse(msg: BridgeMessage): msg is ToolCallResponse {
return 'result' in msg || 'error' in msg;
}

View file

@ -1,397 +0,0 @@
import { WebSocketServer, WebSocket } from 'ws';
import { createServer as createNetServer } from 'net';
import { BridgeMessage, isRequest, isResponse } from './protocol.js';
import { v4 as uuidv4 } from 'uuid';
/**
* Codebase context sent from the GitNexus browser app
*/
export interface CodebaseContext {
projectName: string;
stats: {
fileCount: number;
functionCount: number;
classCount: number;
interfaceCount: number;
methodCount: number;
};
hotspots: Array<{
name: string;
type: string;
filePath: string;
connections: number;
}>;
folderTree: string;
}
/**
* Check if a Port is available
*/
async function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createNetServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
}
export class WebSocketBridge {
private wss: WebSocketServer | null = null; // Used if we are the Hub
private client: WebSocket | null = null; // Used if we are a Peer (connecting to Hub), OR if we are Hub (clients connecting to us)
// Hub State
private browserClient: WebSocket | null = null;
private peerClients: Map<string, WebSocket> = new Map();
// Common State
private pendingRequests: Map<string, { resolve: (val: any) => void, reject: (err: any) => void }> = new Map();
private requestId = 0;
private started = false;
private _context: any | null = null; // CodebaseContext
private contextListeners: Set<(context: any | null) => void> = new Set();
private agentName: string;
private isHub = false;
private port = 54319;
constructor(port: number = 54319, agentName?: string) {
this.port = port;
this.agentName = agentName || process.env.GITNEXUS_AGENT || this.detectAgent();
}
private detectAgent(): string {
if (process.env.CURSOR_SESSION_ID) return 'Cursor';
if (process.env.CLAUDE_CODE) return 'Claude Code';
if (process.env.WINDSURF_SESSION) return 'Windsurf';
return 'Unknown Agent';
}
async start(): Promise<boolean> {
const available = await isPortAvailable(this.port);
if (available) {
return this.startAsHub();
} else {
return this.startAsPeer();
}
}
// -------------------------------------------------------------------------
// Hub Implementation (Master)
// -------------------------------------------------------------------------
private async startAsHub(): Promise<boolean> {
console.error(`Starting as MCP Hub on port ${this.port}`);
this.isHub = true;
return new Promise((resolve) => {
this.wss = new WebSocketServer({ port: this.port });
this.wss.on('connection', (ws, req) => {
// Security: Origin check could go here if req.headers.origin available
ws.on('message', (data) => this.handleHubMessage(ws, data));
ws.on('close', () => this.handleHubDisconnect(ws));
ws.on('error', (err) => console.error('Hub client error:', err));
});
this.wss.on('listening', () => {
this.started = true;
resolve(true);
});
this.wss.on('error', (err) => {
console.error('Hub server error:', err);
resolve(false);
});
});
}
private handleHubMessage(ws: WebSocket, data: any) {
try {
const msg: BridgeMessage = JSON.parse(data.toString());
if (msg.type === 'handshake') {
// Peer verifying we are GitNexus
ws.send(JSON.stringify({ type: 'handshake_ack', id: msg.id }));
return;
}
if (msg.type === 'register_peer') {
// Peer registering itself
const peerId = uuidv4();
this.peerClients.set(peerId, ws);
(ws as any).peerId = peerId;
(ws as any).agentName = msg.agentName;
console.error(`Peer connected: ${msg.agentName} (${peerId})`);
// Forward current context to new peer if available
if (this._context) {
ws.send(JSON.stringify({ type: 'context', params: this._context }));
}
return;
}
// Handle Context updates (from Browser)
if (msg.type === 'context') {
// Browser identified itself (implicitly)
if (this.browserClient !== ws) {
if (this.browserClient) this.browserClient.close();
this.browserClient = ws;
console.error('Browser connected to Hub');
}
this._context = msg.params;
this.notifyContextListeners();
// Broadcast context to all peers
this.broadcastToPeers(msg);
return;
}
// Handle Tool Calls (Peer/Hub -> Browser)
if (isRequest(msg)) {
// If it came from a ws client (Peer), validation needed?
// We assume it's destined for the Browser
if (this.browserClient && this.browserClient.readyState === WebSocket.OPEN) {
// Attach agent info if missing (for UI)
if (!msg.agentName && (ws as any).agentName) {
msg.agentName = (ws as any).agentName;
}
// Attach peerId so we can route response back
if (!msg.peerId && (ws as any).peerId) {
msg.peerId = (ws as any).peerId;
}
this.browserClient.send(JSON.stringify(msg));
} else {
// Browser not connected, fail
if (msg.id) {
ws.send(JSON.stringify({
id: msg.id,
error: { message: "Browser not connected. Open GitNexus." }
}));
}
}
return;
}
// Handle Tool Results (Browser -> Peer/Hub)
if (isResponse(msg)) {
// Route to the correct peer
if (msg.peerId && this.peerClients.has(msg.peerId)) {
const peer = this.peerClients.get(msg.peerId);
if (peer?.readyState === WebSocket.OPEN) {
peer.send(JSON.stringify(msg));
}
} else {
// It might be for Us (the Hub)
this.handleResponseLocal(msg);
}
return;
}
} catch (e) {
console.error('Hub: Failed to parse message', e);
}
}
private handleHubDisconnect(ws: WebSocket) {
if (ws === this.browserClient) {
console.error('Browser disconnected from Hub');
this.browserClient = null;
this._context = null;
this.notifyContextListeners();
} else {
const peerId = (ws as any).peerId;
if (peerId) {
this.peerClients.delete(peerId);
console.error(`Peer disconnected: ${peerId}`);
}
}
}
private broadcastToPeers(msg: any) {
for (const client of this.peerClients.values()) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg));
}
}
}
// -------------------------------------------------------------------------
// Peer Implementation (Spoke)
// -------------------------------------------------------------------------
private async startAsPeer(): Promise<boolean> {
console.error(`Port ${this.port} busy. Attempting to connect as Peer...`);
return new Promise((resolve) => {
const ws = new WebSocket(`ws://localhost:${this.port}`);
const timeout = setTimeout(() => {
console.error('Handshake timeout. Port is busy by unknown app.');
ws.close();
resolve(false);
}, 1000);
ws.on('open', () => {
// Send Handshake
ws.send(JSON.stringify({ type: 'handshake', id: 'init' }));
});
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
// Handshake success?
if (msg.type === 'handshake_ack') {
clearTimeout(timeout);
console.error('Handshake successful. Joining as Peer.');
// Register ourselves
ws.send(JSON.stringify({
type: 'register_peer',
agentName: this.agentName
}));
this.client = ws;
this.started = true;
resolve(true);
return;
}
// Normal messages from Hub
this.handlePeerMessage(msg);
} catch (e) {
// ignore garbage
}
});
ws.on('error', (err) => {
console.error('Peer connection error:', err);
resolve(false);
});
// If connection fails immediately
ws.on('close', () => {
if (!this.started) resolve(false);
else {
this.client = null;
this._context = null;
this.notifyContextListeners();
}
});
});
}
private handlePeerMessage(msg: BridgeMessage) {
if (msg.type === 'context') {
this._context = msg.params;
this.notifyContextListeners();
return;
}
if (isResponse(msg)) {
this.handleResponseLocal(msg);
}
}
// -------------------------------------------------------------------------
// Shared / Public API
// -------------------------------------------------------------------------
private handleResponseLocal(msg: any) {
if (msg.id && this.pendingRequests.has(msg.id)) {
const { resolve, reject } = this.pendingRequests.get(msg.id)!;
this.pendingRequests.delete(msg.id);
if (msg.error) {
// We'll reject the promise so caller knows
reject(new Error(msg.error.message));
} else {
resolve(msg.result);
}
}
}
get isConnected(): boolean {
if (this.isHub) {
return this.browserClient !== null && this.browserClient.readyState === WebSocket.OPEN;
} else {
return this.client !== null && this.client.readyState === WebSocket.OPEN;
}
}
get context(): any {
return this._context;
}
onContextChange(listener: (context: any) => void) {
this.contextListeners.add(listener);
return () => this.contextListeners.delete(listener);
}
private notifyContextListeners() {
this.contextListeners.forEach((listener) => listener(this._context));
}
async callTool(method: string, params: any): Promise<any> {
if (!this.isConnected) {
if (this.isHub) throw new Error('GitNexus Browser not connected.');
else throw new Error('GitNexus Hub disonnected.');
}
const id = `req_${++this.requestId}`;
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
const msg: BridgeMessage = {
id,
method,
params,
agentName: this.agentName,
// type is implicitly request because of method
};
if (this.isHub) {
// Send directly to browser
if (this.browserClient && this.browserClient.readyState === WebSocket.OPEN) {
this.browserClient.send(JSON.stringify(msg));
} else {
this.pendingRequests.delete(id);
reject(new Error('Browser not connected'));
}
} else {
// Send to Hub (who forwards to browser)
if (this.client && this.client.readyState === WebSocket.OPEN) {
this.client.send(JSON.stringify(msg));
} else {
this.pendingRequests.delete(id);
reject(new Error('Hub disconnected'));
}
}
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error('Request timeout'));
}
}, 30000);
});
}
close() {
this.wss?.close();
this.client?.close();
}
disconnect() {
this.close();
}
}

View file

@ -1,60 +0,0 @@
#!/usr/bin/env node
/**
* GitNexus MCP CLI
*
* Bridge between external AI agents (Cursor, Claude Code, Windsurf)
* and GitNexus code intelligence running in the browser.
*/
import { serveCommand } from './commands/serve.js';
/**
* Minimal CLI:
* - Default: start MCP stdio server + local browser WebSocket bridge
* - Optional: `serve` alias, and `--port <port>`
*
* This is designed for MCP clients (Cursor/Claude/Windsurf) which spawn this
* process automatically; users should not need to run commands manually.
*/
function parsePort(argv: string[]): string {
const portFlagIndex = argv.findIndex((a) => a === '--port' || a === '-p');
if (portFlagIndex !== -1) {
const value = argv[portFlagIndex + 1];
if (value) return value;
}
// Support `--port=54319`
const portEq = argv.find((a) => a.startsWith('--port='));
if (portEq) return portEq.split('=')[1] || '54319';
return '54319';
}
async function main() {
const argv = process.argv.slice(2);
const first = argv[0];
const port = parsePort(argv);
// Allow `gitnexus-mcp serve` for compatibility, but default to serve anyway
if (!first || first === 'serve') {
await serveCommand({ port });
return;
}
// Minimal help for unknown commands
if (first === '--help' || first === '-h') {
// eslint-disable-next-line no-console
console.log('gitnexus-mcp\n\nUsage:\n gitnexus-mcp [serve] [--port <port>]\n');
process.exit(0);
}
// eslint-disable-next-line no-console
console.error(`Unknown command: ${first}`);
// eslint-disable-next-line no-console
console.error('Usage: gitnexus-mcp [serve] [--port <port>]');
process.exit(1);
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});

View file

@ -1,31 +0,0 @@
/**
* Serve Command
*
* Starts the MCP server that bridges external AI agents to GitNexus.
* - Listens on stdio for MCP protocol (from AI tools)
* - Hosts a local WebSocket bridge for the GitNexus browser app
*/
import { startMCPServer } from '../mcp/server.js';
import { WebSocketBridge } from '../bridge/websocket-server.js';
interface ServeOptions {
port: string;
}
export async function serveCommand(options: ServeOptions) {
const port = parseInt(options.port, 10);
// Start local WebSocket bridge (browser connects to ws://localhost:<port>)
const client = new WebSocketBridge(port);
const started = await client.start();
if (!started) {
console.error(`Failed to start GitNexus browser bridge on port ${port}.`);
console.error('Another process is already using this port.');
process.exit(1);
}
// Start MCP server on stdio (AI tools connect here)
await startMCPServer(client);
}

View file

@ -1,226 +0,0 @@
/**
* MCP Server
*
* Model Context Protocol server that runs on stdio.
* External AI tools (Cursor, Claude Code) spawn this process and
* communicate via stdin/stdout using the MCP protocol.
*
* Exposes:
* - Tools: search, cypher, blastRadius, highlight
* - Resources: codebase context (stats, hotspots, folder tree)
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { GITNEXUS_TOOLS } from './tools.js';
import type { CodebaseContext } from '../bridge/websocket-server.js';
// Interface for anything that can call tools (DaemonClient or WebSocketBridge)
interface ToolCaller {
callTool(method: string, params: any): Promise<any>;
disconnect?(): void;
context?: CodebaseContext | null;
onContextChange?: (listener: (context: CodebaseContext | null) => void) => () => void;
}
/**
* Format context as markdown for the resource
*/
function formatContextAsMarkdown(context: CodebaseContext): string {
const { projectName, stats, hotspots, folderTree } = context;
const lines: string[] = [];
lines.push(`# GitNexus: ${projectName}`);
lines.push('');
lines.push('This codebase is currently loaded in GitNexus. Use the tools below to explore it.');
lines.push('');
// Stats
lines.push('## 📊 Statistics');
lines.push(`- **Files**: ${stats.fileCount}`);
lines.push(`- **Functions**: ${stats.functionCount}`);
if (stats.classCount > 0) lines.push(`- **Classes**: ${stats.classCount}`);
if (stats.interfaceCount > 0) lines.push(`- **Interfaces**: ${stats.interfaceCount}`);
if (stats.methodCount > 0) lines.push(`- **Methods**: ${stats.methodCount}`);
lines.push('');
// Hotspots
if (hotspots.length > 0) {
lines.push('## 🔥 Hotspots (Most Connected Nodes)');
lines.push('');
hotspots.forEach(h => {
lines.push(`- \`${h.name}\` (${h.type}) — ${h.connections} connections — ${h.filePath}`);
});
lines.push('');
}
// Folder tree
if (folderTree) {
lines.push('## 📁 Project Structure');
lines.push('```');
lines.push(projectName + '/');
lines.push(folderTree);
lines.push('```');
lines.push('');
}
// Usage hints
lines.push('## 🛠️ Available Tools');
lines.push('');
lines.push('- **search**: Semantic + keyword search across codebase');
lines.push('- **cypher**: Execute Cypher queries on knowledge graph');
lines.push('- **grep**: Regex pattern search in files');
lines.push('- **read**: Read file contents');
lines.push('- **explore**: Deep dive on symbol, cluster, or process');
lines.push('- **overview**: Codebase map (all clusters + processes)');
lines.push('- **impact**: Analyze change impact (upstream/downstream)');
lines.push('- **highlight**: Visualize nodes in graph');
lines.push('');
lines.push('## 📝 Graph Schema');
lines.push('');
lines.push('**Node Types**: File, Folder, Function, Class, Interface, Method, Community, Process');
lines.push('');
lines.push('**Relation**: `CodeRelation` with `type` property:');
lines.push('- CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES');
lines.push('- MEMBER_OF (symbol → community), STEP_IN_PROCESS (symbol → process)');
lines.push('');
lines.push('**Example Cypher Queries**:');
lines.push('```cypher');
lines.push('MATCH (f:Function) RETURN f.name LIMIT 10');
lines.push("MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name");
lines.push("MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) RETURN c.label, count(s)");
lines.push('```');
return lines.join('\n');
}
export async function startMCPServer(client: ToolCaller): Promise<void> {
const server = new Server(
{
name: 'gitnexus',
version: '0.1.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Handle list resources request
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const context = client.context;
if (!context) {
return { resources: [] };
}
return {
resources: [
{
uri: 'gitnexus://codebase/context',
name: `GitNexus: ${context.projectName}`,
description: `Codebase context for ${context.projectName} (${context.stats.fileCount} files, ${context.stats.functionCount} functions)`,
mimeType: 'text/markdown',
},
],
};
});
// Handle read resource request
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'gitnexus://codebase/context') {
const context = client.context;
if (!context) {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: 'No codebase loaded. Open GitNexus in your browser and load a repository.',
},
],
};
}
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: formatContextAsMarkdown(context),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
// Handle list tools request
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: GITNEXUS_TOOLS.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// Forward the tool call to the browser via daemon
const result = await client.callTool(name, args);
return {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: `Error: ${message}`,
},
],
isError: true,
};
}
});
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
// Handle graceful shutdown
process.on('SIGINT', async () => {
client.disconnect?.();
await server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
client.disconnect?.();
await server.close();
process.exit(0);
});
}

View file

@ -1,225 +0,0 @@
/**
* MCP Tool Definitions
*
* Defines the tools that GitNexus exposes to external AI agents.
* Each tool has a rich description with examples to help agents use them correctly.
*/
export interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, {
type: string;
description?: string;
default?: any;
items?: { type: string };
}>;
required: string[];
};
}
export const GITNEXUS_TOOLS: ToolDefinition[] = [
{
name: 'context',
description: `Get GitNexus codebase context. CALL THIS FIRST before using other tools.
Returns:
- Project name and stats (files, functions, classes)
- Hotspots (most connected/important nodes)
- Directory structure (TOON format for token efficiency)
- Tool usage guidance
ALWAYS call this first to understand the codebase before searching or querying.`,
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'search',
description: `Hybrid search (keyword + semantic) across the codebase.
Returns code nodes with their graph connections, grouped by process.
WHEN TO USE:
- Finding implementations ("where is auth handled?")
- Understanding code flow ("what calls UserService?")
- Locating patterns ("find all API endpoints")
RETURNS: Array of {name, type, filePath, code, connections[], cluster, processes[]}`,
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Natural language or keyword search query' },
limit: { type: 'number', description: 'Max results to return', default: 10 },
groupByProcess: { type: 'boolean', description: 'Group results by process', default: true },
},
required: ['query'],
},
},
{
name: 'cypher',
description: `Execute Cypher query against the code knowledge graph.
SCHEMA:
- Nodes: File, Folder, Function, Class, Interface, Method, Community, Process
- Edges via CodeRelation.type: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
EXAMPLES:
Find callers of a function:
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
Find all functions in a community:
MATCH (f:Function)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community {label: "Auth"}) RETURN f.name
Find steps in a process:
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {label: "UserLogin"}) RETURN s.name, r.step ORDER BY r.step
TIPS:
- All relationships use CodeRelation table with 'type' property
- Community = functional cluster detected by Leiden algorithm
- Process = execution flow trace from entry point to terminal`,
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Cypher query to execute' },
},
required: ['query'],
},
},
{
name: 'grep',
description: `Regex search for exact patterns in file contents.
WHEN TO USE:
- Finding exact strings: error codes, TODOs, specific API keys
- Pattern matching: all console.log, all fetch calls
- Finding imports of specific modules
BETTER THAN search for: exact matches, regex patterns, case-sensitive
RETURNS: Array of {filePath, line, lineNumber, match}`,
inputSchema: {
type: 'object',
properties: {
pattern: { type: 'string', description: 'Regex pattern to search for' },
caseSensitive: { type: 'boolean', description: 'Case-sensitive search', default: false },
maxResults: { type: 'number', description: 'Max results to return', default: 50 },
},
required: ['pattern'],
},
},
{
name: 'read',
description: `Read file content from the codebase.
WHEN TO USE:
- After search/grep to see full context
- To understand implementation details
- Before making changes
ALWAYS read before concluding - don't guess from names alone.
RETURNS: {filePath, content, language, lines}`,
inputSchema: {
type: 'object',
properties: {
filePath: { type: 'string', description: 'Path to file to read' },
startLine: { type: 'number', description: 'Start line (optional)' },
endLine: { type: 'number', description: 'End line (optional)' },
},
required: ['filePath'],
},
},
{
name: 'explore',
description: `Deep dive on a symbol, cluster, or process.
TYPE: symbol | cluster | process
For SYMBOL: Shows cluster membership, process participation, callers/callees
For CLUSTER: Shows members, cohesion score, processes touching it
For PROCESS: Shows step-by-step trace, clusters traversed, entry/terminal points
Use after search to understand context of a specific node.`,
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name of symbol, cluster, or process to explore' },
type: { type: 'string', description: 'Type: symbol, cluster, or process' },
},
required: ['name', 'type'],
},
},
{
name: 'overview',
description: `Get codebase map showing all clusters and processes.
Returns:
- All communities (clusters) with member counts and cohesion scores
- All processes with step counts and types (intra/cross-community)
- High-level architectural view
Use to understand overall codebase structure before diving deep.`,
inputSchema: {
type: 'object',
properties: {
showProcesses: { type: 'boolean', description: 'Include process list', default: true },
showClusters: { type: 'boolean', description: 'Include cluster list', default: true },
limit: { type: 'number', description: 'Max items per category', default: 20 },
},
required: [],
},
},
{
name: 'impact',
description: `Analyze the impact of changing a code element.
Returns all nodes affected by modifying the target, with distance, edge type, and confidence.
USE BEFORE making changes to understand ripple effects.
Output includes:
- Affected processes (with step positions)
- Affected clusters (direct/indirect)
- Risk assessment (critical/high/medium/low)
- Callers/dependents grouped by depth
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS
Confidence: 100% = certain, <80% = fuzzy match
Depth groups:
- d=1: WILL BREAK (direct callers/importers)
- d=2: LIKELY AFFECTED (indirect)
- d=3: MAY NEED TESTING (transitive)`,
inputSchema: {
type: 'object',
properties: {
target: { type: 'string', description: 'Name of function, class, or file to analyze' },
direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (default: usage-based)' },
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
},
required: ['target', 'direction'],
},
},
{
name: 'highlight',
description: `Highlight nodes in the GitNexus graph visualization.
Use after search/analysis to show the user what you found.
The user will see the nodes glow in the graph view.
Great for visual confirmation of your findings.`,
inputSchema: {
type: 'object',
properties: {
nodeIds: { type: 'array', items: { type: 'string' }, description: 'Array of node IDs to highlight' },
color: { type: 'string', description: 'Highlight color (optional, default: cyan)' },
},
required: ['nodeIds'],
},
},
];

View file

@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [
"ES2022"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

7
gitnexus-test-setup/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# GitNexus AI Context
.gitnexus-rules.md
.cursorrules
.windsurfrules
CLAUDE.md
.github/copilot-instructions.md

2
gitnexus-web/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vercel
.env*.local

View file

@ -55,9 +55,9 @@
**Goal:** Group related code into named clusters.
### 1.1 Research & Setup
- [ ] Research JS/WASM implementations of Leiden algorithm
- Options: `graphology-communities-louvain`, custom WASM port
- Constraint: Must run in browser
- [x] Implement Leiden algorithm for community detection
- Vendored from graphology-communities-leiden (unpublished npm, MIT licensed)
- Works in both browser (ESM) and Node.js (CJS)
- [ ] Benchmark on sample codebases (100, 1K, 10K nodes)
### 1.2 Schema Updates
@ -324,10 +324,9 @@ interface ImpactResult {
## Technical Notes
### Leiden Algorithm Options
1. **graphology-communities-louvain** (JS, works in browser)
2. **Custom WASM port** (if performance needed)
3. **Simple Louvain** might be sufficient for V1
### Leiden Algorithm
Implemented using vendored graphology-communities-leiden source (MIT licensed).
The Leiden algorithm guarantees well-connected communities via a refinement phase after each Louvain-style move phase.
### Schema Summary (New Additions)
```

10203
gitnexus-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

70
gitnexus-web/package.json Normal file
View file

@ -0,0 +1,70 @@
{
"name": "gitnexus",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@huggingface/transformers": "^3.0.0",
"@isomorphic-git/lightning-fs": "^4.6.2",
"@langchain/anthropic": "^1.3.10",
"@langchain/core": "^1.1.15",
"@langchain/google-genai": "^2.1.10",
"@langchain/langgraph": "^1.1.0",
"@langchain/ollama": "^1.2.0",
"@langchain/openai": "^1.2.2",
"@sigma/edge-curve": "^3.1.0",
"@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2",
"buffer": "^6.0.3",
"comlink": "^4.4.2",
"d3": "^7.9.0",
"graphology": "^0.26.0",
"graphology-indices": "^0.17.0",
"graphology-utils": "^2.3.0",
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.0",
"graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
"isomorphic-git": "^1.36.1",
"jszip": "^3.10.1",
"kuzu-wasm": "^0.11.1",
"langchain": "^1.2.10",
"lru-cache": "^11.2.4",
"lucide-react": "^0.562.0",
"mermaid": "^11.12.2",
"minisearch": "^7.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"react-zoom-pan-pinch": "^3.7.0",
"remark-gfm": "^4.0.1",
"sigma": "^3.0.2",
"tailwindcss": "^4.1.18",
"uuid": "^13.0.0",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"web-tree-sitter": "^0.20.8",
"zod": "^3.25.76"
},
"devDependencies": {
"@babel/types": "^7.28.5",
"@types/jszip": "^3.4.0",
"@types/node": "^24.10.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/node": "^5.5.16",
"@vitejs/plugin-react": "^5.1.0",
"tree-sitter-wasms": "^0.1.13",
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-plugin-static-copy": "^3.1.4"
}
}

View file

@ -1,4 +1,4 @@
import { useCallback, useRef, useState, useEffect } from 'react';
import { useCallback, useRef } from 'react';
import { AppStateProvider, useAppState } from './hooks/useAppState';
import { DropZone } from './components/DropZone';
import { LoadingOverlay } from './components/LoadingOverlay';
@ -11,8 +11,6 @@ import { FileTreePanel } from './components/FileTreePanel';
import { CodeReferencesPanel } from './components/CodeReferencesPanel';
import { FileEntry } from './services/zip';
import { getActiveProviderConfig } from './core/llm/settings-service';
import { ProviderConfig } from './core/llm/types';
import { IntelligentClusteringModal } from './components/IntelligentClusteringModal';
const AppContent = () => {
const {
@ -31,68 +29,24 @@ const AppContent = () => {
refreshLLMSettings,
initializeAgent,
startEmbeddings,
startBackgroundEnrichment,
embeddingStatus,
codeReferences,
selectedNode,
isCodePanelOpen,
llmSettings,
updateLLMSettings,
runClusterEnrichment,
} = useAppState();
const [showClusteringModal, setShowClusteringModal] = useState(false);
// Trigger clustering modal after ingestion if not seen yet
// DISABLED: Clustering is now in the upload flow
/*
useEffect(() => {
if (viewMode === 'exploring' && !llmSettings.hasSeenClusteringPrompt && !llmSettings.intelligentClustering) {
const timer = setTimeout(() => setShowClusteringModal(true), 2000);
return () => clearTimeout(timer);
}
}, [viewMode, llmSettings.hasSeenClusteringPrompt, llmSettings.intelligentClustering]);
*/
const handleEnableClustering = useCallback(() => {
updateLLMSettings({
intelligentClustering: true,
hasSeenClusteringPrompt: true,
useSameModelForClustering: true // Default to simple path
});
setShowClusteringModal(false);
runClusterEnrichment().catch(console.error);
}, [updateLLMSettings, runClusterEnrichment]);
const handleConfigureClustering = useCallback(() => {
updateLLMSettings({ hasSeenClusteringPrompt: true });
setShowClusteringModal(false);
setSettingsPanelOpen(true);
}, [updateLLMSettings, setSettingsPanelOpen]);
const handleSkipClustering = useCallback(() => {
updateLLMSettings({ hasSeenClusteringPrompt: true });
setShowClusteringModal(false);
}, [updateLLMSettings]);
const graphCanvasRef = useRef<GraphCanvasHandle>(null);
const handleFileSelect = useCallback(async (file: File, enableSmartClustering?: boolean) => {
console.log('📥 App.handleFileSelect - param received:', enableSmartClustering, 'provider exists:', !!getActiveProviderConfig());
const handleFileSelect = useCallback(async (file: File) => {
const projectName = file.name.replace('.zip', '');
setProjectName(projectName);
// Set initial progress BEFORE entering loading mode to prevent black screen
setProgress({ phase: 'extracting', percent: 0, message: 'Starting...', detail: 'Preparing to extract files' });
setViewMode('loading');
try {
// Prepare LLM config if clustering is enabled
const clusteringConfig = enableSmartClustering ? getActiveProviderConfig() ?? undefined : undefined;
console.log('✅ clusteringConfig:', !!clusteringConfig, clusteringConfig?.provider);
const result = await runPipeline(file, (progress) => {
setProgress(progress);
}, clusteringConfig || undefined);
});
setGraph(result.graph);
setFileContents(result.fileContents);
@ -107,16 +61,12 @@ const AppContent = () => {
// Auto-start embeddings pipeline in background
// Uses WebGPU if available, falls back to WASM
startEmbeddings().catch((err) => {
// WebGPU not available - try WASM fallback silently
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
}
});
// Start background cluster enrichment (if toggle was enabled)
startBackgroundEnrichment().catch(console.warn);
} catch (error) {
console.error('Pipeline error:', error);
setProgress({
@ -130,49 +80,36 @@ const AppContent = () => {
setProgress(null);
}, 3000);
}
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddings, initializeAgent, llmSettings]);
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipeline, startEmbeddings, initializeAgent]);
const handleGitClone = useCallback(async (files: FileEntry[], enableSmartClustering?: boolean) => {
// Extract project name from first file path (e.g., "owner-repo-123/src/..." -> "owner-repo")
const handleGitClone = useCallback(async (files: FileEntry[]) => {
const firstPath = files[0]?.path || 'repository';
const projectName = firstPath.split('/')[0].replace(/-\d+$/, '') || 'repository';
setProjectName(projectName);
// Set initial progress BEFORE entering loading mode to prevent black screen
setProgress({ phase: 'extracting', percent: 0, message: 'Starting...', detail: 'Preparing to process files' });
setViewMode('loading');
try {
// Prepare LLM config if clustering is enabled
const clusteringConfig = enableSmartClustering ? getActiveProviderConfig() ?? undefined : undefined;
const result = await runPipelineFromFiles(files, (progress) => {
setProgress(progress);
}, clusteringConfig || undefined);
});
setGraph(result.graph);
setFileContents(result.fileContents);
setViewMode('exploring');
// Initialize (or re-initialize) the agent AFTER a repo loads so it captures
// the current codebase context (file contents + graph tools) in the worker.
if (getActiveProviderConfig()) {
initializeAgent(projectName);
}
// Auto-start embeddings pipeline in background
// Uses WebGPU if available, falls back to WASM
startEmbeddings().catch((err) => {
// WebGPU not available - try WASM fallback silently
if (err?.name === 'WebGPUNotAvailableError' || err?.message?.includes('WebGPU')) {
startEmbeddings('wasm').catch(console.warn);
} else {
console.warn('Embeddings auto-start failed:', err);
}
});
// Start background cluster enrichment (if toggle was enabled)
startBackgroundEnrichment().catch(console.warn);
} catch (error) {
console.error('Pipeline error:', error);
setProgress({
@ -186,7 +123,7 @@ const AppContent = () => {
setProgress(null);
}, 3000);
}
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent, runClusterEnrichment]);
}, [setViewMode, setGraph, setFileContents, setProgress, setProjectName, runPipelineFromFiles, startEmbeddings, initializeAgent]);
const handleFocusNode = useCallback((nodeId: string) => {
graphCanvasRef.current?.focusNode(nodeId);
@ -242,13 +179,6 @@ const AppContent = () => {
onSettingsSaved={handleSettingsSaved}
/>
{/* Intelligent Clustering Modal */}
<IntelligentClusteringModal
isOpen={showClusteringModal}
onClose={handleSkipClustering}
onEnable={handleEnableClustering}
onConfigure={handleConfigureClustering}
/>
</div>
);
};

View file

@ -1,12 +1,11 @@
import { useState, useCallback, DragEvent, useEffect } from 'react';
import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff, Sparkles } from 'lucide-react';
import { useState, useCallback, DragEvent } from 'react';
import { Upload, FileArchive, Github, Loader2, ArrowRight, Key, Eye, EyeOff } from 'lucide-react';
import { cloneRepository, parseGitHubUrl } from '../services/git-clone';
import { FileEntry } from '../services/zip';
import { getActiveProviderConfig } from '../core/llm/settings-service';
interface DropZoneProps {
onFileSelect: (file: File, enableSmartClustering?: boolean) => void;
onGitClone?: (files: FileEntry[], enableSmartClustering?: boolean) => void;
onFileSelect: (file: File) => void;
onGitClone?: (files: FileEntry[]) => void;
}
export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
@ -18,15 +17,6 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
const [isCloning, setIsCloning] = useState(false);
const [cloneProgress, setCloneProgress] = useState({ phase: '', percent: 0 });
const [error, setError] = useState<string | null>(null);
const [enableSmartClustering, setEnableSmartClustering] = useState(false);
const [hasLLMProvider, setHasLLMProvider] = useState(false);
// Check if LLM provider is configured
useEffect(() => {
const config = getActiveProviderConfig();
setHasLLMProvider(!!config);
// Keep smart clustering OFF by default, user must opt-in
}, []);
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
@ -49,25 +39,24 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
if (files.length > 0) {
const file = files[0];
if (file.name.endsWith('.zip')) {
onFileSelect(file, enableSmartClustering);
onFileSelect(file);
} else {
setError('Please drop a .zip file');
}
}
}, [onFileSelect, enableSmartClustering]);
}, [onFileSelect]);
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
const file = files[0];
if (file.name.endsWith('.zip')) {
console.log('🎯 DropZone: Calling onFileSelect with enableSmartClustering:', enableSmartClustering);
onFileSelect(file, enableSmartClustering);
onFileSelect(file);
} else {
setError('Please select a .zip file');
}
}
}, [onFileSelect, enableSmartClustering]);
}, [onFileSelect]);
const handleGitClone = async () => {
if (!githubUrl.trim()) {
@ -96,7 +85,7 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
setGithubToken('');
if (onGitClone) {
onGitClone(files, enableSmartClustering);
onGitClone(files);
}
} catch (err) {
console.error('Clone failed:', err);
@ -224,41 +213,6 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => {
</div>
</div>
{/* Smart Clustering Toggle - Below drop zone */}
<div className="mt-4 p-4 bg-surface/50 border border-border-subtle rounded-xl">
<label className="flex items-center justify-between cursor-pointer group">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Sparkles className="w-4 h-4 text-accent" />
<span className="text-sm font-medium text-text-primary">Enable Smart Clustering</span>
</div>
<p className="text-xs text-text-muted leading-relaxed">
Uses LLM to label processes and clusters for better detection. Don't worry, it consumes very less tokens one time.
</p>
{!hasLLMProvider && (
<p className="text-xs text-amber-400 mt-2">
Setup LLM provider to enable smart clustering
</p>
)}
</div>
<button
type="button"
onClick={() => hasLLMProvider && setEnableSmartClustering(!enableSmartClustering)}
disabled={!hasLLMProvider}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors ml-4
${enableSmartClustering && hasLLMProvider ? 'bg-accent' : 'bg-gray-700'}
${!hasLLMProvider ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${enableSmartClustering && hasLLMProvider ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</label>
</div>
</>
)}

Some files were not shown because too many files have changed in this diff Show more