mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
commit
e79133daaa
254 changed files with 38577 additions and 28804 deletions
75
.claude/settings.local.json
Normal file
75
.claude/settings.local.json
Normal 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"
|
||||
]
|
||||
}
|
||||
85
.claude/skills/gitnexus/debugging/SKILL.md
Normal file
85
.claude/skills/gitnexus/debugging/SKILL.md
Normal 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
|
||||
```
|
||||
75
.claude/skills/gitnexus/exploring/SKILL.md
Normal file
75
.claude/skills/gitnexus/exploring/SKILL.md
Normal 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
|
||||
```
|
||||
94
.claude/skills/gitnexus/impact-analysis/SKILL.md
Normal file
94
.claude/skills/gitnexus/impact-analysis/SKILL.md
Normal 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
|
||||
```
|
||||
113
.claude/skills/gitnexus/refactoring/SKILL.md
Normal file
113
.claude/skills/gitnexus/refactoring/SKILL.md
Normal 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
|
||||
```
|
||||
1
.claude/worktrees/determined-hofstadter
Submodule
1
.claude/worktrees/determined-hofstadter
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e90622aa24c92d7a08712085d2355b9fe36cf3c3
|
||||
1
.claude/worktrees/quirky-stonebraker
Submodule
1
.claude/worktrees/quirky-stonebraker
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e90622aa24c92d7a08712085d2355b9fe36cf3c3
|
||||
1
.claude/worktrees/sweet-faraday
Submodule
1
.claude/worktrees/sweet-faraday
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 44572ad0bddea03d548b5f25ace720ba4f4c1548
|
||||
|
|
@ -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
5
.cursorrules
Normal 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
7
.gitignore
vendored
|
|
@ -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
9
.mcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": ["/c", "npx", "-y", "gitnexus@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
5
.windsurfrules
Normal file
5
.windsurfrules
Normal 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
64
AGENTS.md
Normal 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 -->
|
||||
732
ARCHITECTURE.md
732
ARCHITECTURE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -374,3 +374,11 @@ model = pybamm.lithium_ion.DFN(
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
62
CLAUDE.md
Normal file
62
CLAUDE.md
Normal 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 -->
|
||||
|
|
@ -373,3 +373,11 @@ Expression tree traversal:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -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
|
||||
|
||||
|
|
|
|||
23
eval/.env.example
Normal file
23
eval/.env.example
Normal 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
16
eval/.gitignore
vendored
Normal 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
210
eval/README.md
Normal 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
1
eval/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# GitNexus SWE-bench Evaluation Harness
|
||||
0
eval/agents/__init__.py
Normal file
0
eval/agents/__init__.py
Normal file
209
eval/agents/gitnexus_agent.py
Normal file
209
eval/agents/gitnexus_agent.py
Normal 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),
|
||||
}
|
||||
0
eval/analysis/__init__.py
Normal file
0
eval/analysis/__init__.py
Normal file
445
eval/analysis/analyze_results.py
Normal file
445
eval/analysis/analyze_results.py
Normal 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
0
eval/bridge/__init__.py
Normal file
155
eval/bridge/gitnexus_tools.sh
Normal file
155
eval/bridge/gitnexus_tools.sh
Normal 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
336
eval/bridge/mcp_bridge.py
Normal 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))
|
||||
9
eval/configs/models/claude-haiku.yaml
Normal file
9
eval/configs/models/claude-haiku.yaml
Normal 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
|
||||
9
eval/configs/models/claude-opus.yaml
Normal file
9
eval/configs/models/claude-opus.yaml
Normal 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
|
||||
9
eval/configs/models/claude-sonnet.yaml
Normal file
9
eval/configs/models/claude-sonnet.yaml
Normal 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
|
||||
7
eval/configs/models/glm-4.7.yaml
Normal file
7
eval/configs/models/glm-4.7.yaml
Normal 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
|
||||
7
eval/configs/models/glm-5.yaml
Normal file
7
eval/configs/models/glm-5.yaml
Normal 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
|
||||
7
eval/configs/models/minimax-2.5.yaml
Normal file
7
eval/configs/models/minimax-2.5.yaml
Normal 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
|
||||
9
eval/configs/modes/baseline.yaml
Normal file
9
eval/configs/modes/baseline.yaml
Normal 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"
|
||||
19
eval/configs/modes/native.yaml
Normal file
19
eval/configs/modes/native.yaml
Normal 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
|
||||
24
eval/configs/modes/native_augment.yaml
Normal file
24
eval/configs/modes/native_augment.yaml
Normal 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
|
||||
0
eval/environments/__init__.py
Normal file
0
eval/environments/__init__.py
Normal file
397
eval/environments/gitnexus_docker.py
Normal file
397
eval/environments/gitnexus_docker.py
Normal 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
|
||||
80
eval/prompts/instance_baseline.jinja
Normal file
80
eval/prompts/instance_baseline.jinja
Normal 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
|
||||
```
|
||||
102
eval/prompts/instance_native.jinja
Normal file
102
eval/prompts/instance_native.jinja
Normal 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
|
||||
```
|
||||
103
eval/prompts/instance_native_augment.jinja
Normal file
103
eval/prompts/instance_native_augment.jinja
Normal 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
|
||||
```
|
||||
15
eval/prompts/system_baseline.jinja
Normal file
15
eval/prompts/system_baseline.jinja
Normal 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.
|
||||
54
eval/prompts/system_native.jinja
Normal file
54
eval/prompts/system_native.jinja
Normal 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` |
|
||||
56
eval/prompts/system_native_augment.jinja
Normal file
56
eval/prompts/system_native_augment.jinja
Normal 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
35
eval/pyproject.toml
Normal 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
508
eval/run_eval.py
Normal 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()
|
||||
10
gitnexus-claude-plugin/.claude-plugin/plugin.json
Normal file
10
gitnexus-claude-plugin/.claude-plugin/plugin.json
Normal 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"
|
||||
}
|
||||
123
gitnexus-claude-plugin/hooks/gitnexus-hook.js
Normal file
123
gitnexus-claude-plugin/hooks/gitnexus-hook.js
Normal 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();
|
||||
17
gitnexus-claude-plugin/hooks/hooks.json
Normal file
17
gitnexus-claude-plugin/hooks/hooks.json
Normal 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..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
78
gitnexus-claude-plugin/hooks/pre-tool-use.sh
Normal file
78
gitnexus-claude-plugin/hooks/pre-tool-use.sh
Normal 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
|
||||
41
gitnexus-claude-plugin/hooks/session-start.js
Normal file
41
gitnexus-claude-plugin/hooks/session-start.js
Normal 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);
|
||||
42
gitnexus-claude-plugin/hooks/session-start.sh
Normal file
42
gitnexus-claude-plugin/hooks/session-start.sh
Normal 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
|
||||
85
gitnexus-claude-plugin/skills/debugging/SKILL.md
Normal file
85
gitnexus-claude-plugin/skills/debugging/SKILL.md
Normal 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
|
||||
```
|
||||
75
gitnexus-claude-plugin/skills/exploring/SKILL.md
Normal file
75
gitnexus-claude-plugin/skills/exploring/SKILL.md
Normal 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
|
||||
```
|
||||
94
gitnexus-claude-plugin/skills/impact-analysis/SKILL.md
Normal file
94
gitnexus-claude-plugin/skills/impact-analysis/SKILL.md
Normal 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
|
||||
```
|
||||
113
gitnexus-claude-plugin/skills/refactoring/SKILL.md
Normal file
113
gitnexus-claude-plugin/skills/refactoring/SKILL.md
Normal 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
|
||||
```
|
||||
50
gitnexus-cursor-integration/hooks/augment-shell.sh
Normal file
50
gitnexus-cursor-integration/hooks/augment-shell.sh
Normal 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
|
||||
12
gitnexus-cursor-integration/hooks/hooks.json
Normal file
12
gitnexus-cursor-integration/hooks/hooks.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": 1,
|
||||
"hooks": {
|
||||
"beforeShellExecution": [
|
||||
{
|
||||
"command": "./hooks/augment-shell.sh",
|
||||
"timeout": 5,
|
||||
"matcher": "\\brg\\b|\\bgrep\\b"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
113
gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md
Normal file
113
gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md
Normal 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
|
||||
```
|
||||
1762
gitnexus-mcp/package-lock.json
generated
1762
gitnexus-mcp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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
7
gitnexus-test-setup/.gitignore
vendored
Normal 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
2
gitnexus-web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.vercel
|
||||
.env*.local
|
||||
|
|
@ -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
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
70
gitnexus-web/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue