mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
docs: agent development framework, GitHub templates, eval refactor (#479)
* ci: E2E workflow, web typecheck job, pre-commit hook, test suite CI: - ci.yml consolidated to reference ci-tests.yml - ci-quality.yml: add typecheck-web job for gitnexus-web/ - ci-e2e.yml: E2E workflow with dorny/paths-filter (web changes only) - ci-report.yml: remove dead integration-reports references - CI gate allows skipped E2E status - .gitignore: playwright artifacts, eval test artifacts Pre-commit hook: - .githooks/pre-commit: typecheck + unit tests for both packages - Activated via git config core.hooksPath in prepare script Test infrastructure: - Vitest + React Testing Library: 58 unit tests (graph, server-connection, mermaid, settings, constants, utils, paths) - Playwright E2E: 5 tests + manual recording harness - vitest.config from vitest/config, engines.node >= 20 - Playwright artifacts retain-on-failure - wait-on in devDependencies - vitest/coverage-v8 aligned with vitest 4.x Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update gitnexus-web package-lock.json Reflects devDependency additions (vitest, playwright, wait-on, @testing-library, etc.) from package.json changes in this PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): add missing process-list-loaded testid, increase CI timeouts - Add data-testid="process-list-loaded" to ProcessesPanel (E2E tests were waiting for an element that didn't exist) - Increase server connect timeouts from 5s to 10s for slower CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): run gitnexus-web unit tests in CI, remove unused variable - Add gitnexus-web npm ci + vitest run to ci-tests.yml so web unit tests are gated by the CI status check (were only running locally) - Remove unused IS_PLAYWRIGHT_AUTOMATION variable from E2E spec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): add process-row testid, wait for networkidle on page load - Add data-testid="process-row" to ProcessItem component (E2E tests referenced it but it didn't exist in the source) - Use waitUntil: 'networkidle' on page.goto to ensure Vite dev server is fully ready before interacting (fixes first-test timeout in CI) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): add process-view-button and process-highlight-button testids E2E tests referenced these data-testid attributes but they didn't exist in ProcessItem. All 6 E2E testids now have matching source elements: status-ready, process-list-loaded, process-row, process-view-button, process-highlight-button, server-url-input. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): remove networkidle — Vite HMR WebSocket prevents it from resolving networkidle waits for zero network activity for 500ms, but Vite's HMR WebSocket stays open permanently, causing page.goto to timeout at 60s on all tests after the first. The explicit toBeVisible waits on UI elements are sufficient and deterministic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): wait for Server button visibility, add CI retry, all 5 tests pass locally Root cause: test 1 clicked the Server button before React hydrated, so the tab content never rendered and the input wasn't found. Fixes: - Wait for Server button toBeVisible before clicking - Increase input wait to 15s - Remove networkidle (Vite HMR WebSocket prevents it from resolving) - Add retries: 1 in CI for transient cold-start flakiness Verified locally: all 5 E2E tests pass, 198 unit tests pass, typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): tolerate LadybugDB native crash during analyze step gitnexus analyze can crash with "double free or corruption" (known issue #273) during the LadybugDB native addon shutdown. The index is usually written successfully before the crash. The workflow now: 1. Allows analyze to exit non-zero with a warning 2. Verifies .gitnexus index was actually created 3. Only fails if no index exists (real failure) All tests verified locally: 198 unit, 5 E2E pass, typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): fix shell quoting in analyze step, simplify to || true The previous echo string had special characters that broke bash quoting in GitHub Actions. Simplified to: analyze || true, then check if .gitnexus exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add agent development framework, GitHub templates, eval refactor Agent framework (layered docs for AI-assisted contributions): - AGENTS.md: canonical instructions, impact analysis, MCP tools - CLAUDE.md: Claude Code-specific deltas and hooks - GUARDRAILS.md: safety boundaries, non-negotiables, escalation - ARCHITECTURE.md: monorepo layout, data flow map - TESTING.md: test structure, commands, categories - RUNBOOK.md: copy-paste operations for dev/CI/MCP - llms.txt: minimal LLM context pointer Editor integration: - .cursor/index.mdc + rules/100-monorepo.mdc GitHub templates: - PR template with areas-touched checkboxes - Bug report + feature request issue forms Eval harness: - Refactored mcp_bridge, tool_registry, constants - Error sanitization utilities - Property-based tests via Hypothesis Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(eval): use format_exception instead of format_exc in sanitize_exception format_exc() returns the currently handled exception traceback, which may be unrelated if called outside an active except block. Using format_exception(type(exc), exc, exc.__traceback__) reliably captures the passed exception's traceback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update CONTRIBUTING.md and TESTING.md for current CI/hook setup - CONTRIBUTING.md: add gitnexus-web typecheck command, pre-commit hook checklist item - TESTING.md: add gitnexus-web typecheck command, pre-commit hook section (husky), update CI integration to list actual workflow files (ci-quality, ci-tests, ci-e2e) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update testing docs to reflect CI/E2E changes from PR #486 - AGENTS.md: update test counts (CLI ~2000 unit, ~1850 integration), add gitnexus-web testing section (198 unit, 5 E2E with commands) - RUNBOOK.md: fix Node requirement to >=20, fix E2E local repro command - TESTING.md: E2E uses data-testid selectors + real servers, not mocks - .cursor/rules/100-monorepo.mdc: add web test/E2E commands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: address context engineering review — deduplicate tokens, expand Cursor rules - Remove ~100-line gitnexus:start block from CLAUDE.md (was duplicated from AGENTS.md) - Fix gitnexus:start block inlined inside AGENTS.md Reference Docs bullet (doubled) - Replace CLAUDE.md scope table with pointer to AGENTS.md (single source of truth) - Expand .cursor/index.mdc with 5 non-negotiable safety rules for always-on context - Add .cursor/rules/200-eval.mdc with Python/eval commands (glob-scoped to eval/**) - Improve llms.txt with priority annotations and descriptions - Bump version headers to 1.2.0, last-reviewed to 2026-03-24 Saves ~1,400 tokens/session with zero information loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
a191c26571
commit
c68d7975e6
35 changed files with 3985 additions and 280 deletions
21
.cursor/index.mdc
Normal file
21
.cursor/index.mdc
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GitNexus — Cursor project rules
|
||||
|
||||
Last reviewed: 2026-03-24
|
||||
|
||||
Canonical agent instructions: **[AGENTS.md](../AGENTS.md)** (GitNexus MCP rules, monorepo commands, Cursor Cloud notes). **[CLAUDE.md](../CLAUDE.md)** adds Claude Code-specific notes and points back to AGENTS.md for GitNexus.
|
||||
|
||||
## Non-negotiables (always apply)
|
||||
|
||||
- NEVER edit a function/class/method without running `gitnexus_impact` first.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename`.
|
||||
- NEVER commit without running `gitnexus_detect_changes()`.
|
||||
- NEVER ignore HIGH/CRITICAL risk warnings from impact analysis.
|
||||
- NEVER run `npx gitnexus analyze` without `--embeddings` if `.gitnexus/meta.json` shows stored embeddings.
|
||||
|
||||
Full rules: **[AGENTS.md](../AGENTS.md)** (`gitnexus:start` block, Cursor Cloud section).
|
||||
|
||||
**Rule architecture:** Prefer this file plus optional `.cursor/rules/*.mdc` globs (YAML `globs` in frontmatter). Legacy `.cursorrules` is deprecated; content lives here.
|
||||
12
.cursor/rules/100-monorepo.mdc
Normal file
12
.cursor/rules/100-monorepo.mdc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
globs:
|
||||
- "gitnexus/**"
|
||||
- "gitnexus-web/**"
|
||||
---
|
||||
|
||||
# GitNexus build/test quick refs
|
||||
|
||||
- CLI (`gitnexus/`): `npm test`; `npm run test:integration`; `npx tsc --noEmit`.
|
||||
- Web (`gitnexus-web/`): `npm test`; `npm run dev`; `npx tsc -b --noEmit`; `E2E=1 npx playwright test` (needs servers).
|
||||
- `npm install` in `gitnexus/` runs `prepare` (tsc build) and `postinstall` (tree-sitter patches); needs `python3`, `make`, `g++`.
|
||||
- LadybugDB locking tests may fail in containerized environments because of `/tmp` file locks (known issue, not a code bug).
|
||||
14
.cursor/rules/200-eval.mdc
Normal file
14
.cursor/rules/200-eval.mdc
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
globs:
|
||||
- "eval/**"
|
||||
---
|
||||
|
||||
# GitNexus eval harness (Python)
|
||||
|
||||
- **Run tests**: `cd eval && uv run pytest tests/`
|
||||
- **Run with coverage**: `cd eval && uv run coverage run -m pytest tests/ && uv run coverage report`
|
||||
- **Lint**: `cd eval && uv run ruff check .`
|
||||
- **Run eval**: `cd eval && uv run python run_eval.py --config configs/<config>.yaml`
|
||||
- Shared constants live in `eval/constants.py`; tool specs in `eval/tool_registry.py`.
|
||||
- Error logging uses `utils/errors.py` — set `GITNEXUS_EVAL_DEBUG=1` for full tracebacks.
|
||||
- Property-based tests use Hypothesis (`eval/tests/test_property_based.py`).
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# AI Agent Rules
|
||||
# Deprecated for Cursor Agent Mode
|
||||
|
||||
Follow .gitnexus/RULES.md for all project context and coding guidelines.
|
||||
Use **`.cursor/index.mdc`** (`alwaysApply: true`) for project rules. See [AGENTS.md](AGENTS.md).
|
||||
|
||||
This project uses GitNexus MCP for code intelligence. See .gitnexus/RULES.md for available tools and best practices.
|
||||
This file is kept only as a breadcrumb for older workflows.
|
||||
|
|
|
|||
86
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
86
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
name: Bug report
|
||||
description: Report unexpected behavior or a regression
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Goal:** capture enough context to reproduce and fix the issue quickly.
|
||||
Use **one issue per bug**; split unrelated problems.
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Where does the problem show up?
|
||||
options:
|
||||
- gitnexus (CLI / core / indexing / MCP server)
|
||||
- gitnexus-web (browser UI / WASM / workers)
|
||||
- CI / GitHub Actions
|
||||
- Documentation / developer experience
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: One sentence — what went wrong?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: What were you trying to do? Any relevant links, PRs, or commits?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Ordered steps, sample repo or minimal case, commands run.
|
||||
placeholder: |
|
||||
1. …
|
||||
2. …
|
||||
3. …
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: OS, Node version, browser (if web), GitNexus version or commit SHA.
|
||||
placeholder: |
|
||||
- OS:
|
||||
- Node:
|
||||
- Browser (if applicable):
|
||||
- Commit / version:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / screenshots
|
||||
description: Paste errors, stack traces, or attach screenshots (redact secrets).
|
||||
validations:
|
||||
required: false
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: true
|
||||
74
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: Feature request
|
||||
description: Propose a new capability or improvement
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Goal:** describe the problem and desired outcome so maintainers can size and prioritize.
|
||||
Prefer **small, shippable** requests; split large ideas into phases.
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Primary part of the monorepo this relates to.
|
||||
options:
|
||||
- gitnexus (CLI / core / indexing / MCP server)
|
||||
- gitnexus-web (browser UI / WASM / workers)
|
||||
- CI / release / packaging
|
||||
- Documentation / developer experience
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or opportunity
|
||||
description: What pain point or gap exists today?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: What should happen instead? User-visible behavior, APIs, or UX.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Other approaches you considered and why this one is preferred.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance criteria
|
||||
description: Testable conditions for “done” (bullets or checkboxes in prose).
|
||||
placeholder: |
|
||||
- When … then …
|
||||
- Documentation / tests updated where appropriate
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: constraints
|
||||
attributes:
|
||||
label: Constraints
|
||||
description: Compatibility, performance, security, or “must not change” boundaries.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: willing
|
||||
attributes:
|
||||
label: Contribution
|
||||
options:
|
||||
- label: I am willing to open a PR for this (may need design discussion first).
|
||||
required: false
|
||||
52
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
52
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
## Summary
|
||||
|
||||
<!-- One or two sentences: what does this PR change? -->
|
||||
|
||||
## Motivation / context
|
||||
|
||||
<!-- Why is this change needed? Link issues, ADRs, or prior discussion. -->
|
||||
|
||||
## Areas touched
|
||||
|
||||
<!-- Check all that apply -->
|
||||
|
||||
- [ ] `gitnexus/` (CLI / core / MCP server)
|
||||
- [ ] `gitnexus-web/` (Vite / React UI)
|
||||
- [ ] `.github/` (workflows, actions)
|
||||
- [ ] `eval/` or other tooling
|
||||
- [ ] Docs / agent config only (`AGENTS.md`, `CLAUDE.md`, `.cursor/`, `llms.txt`, etc.)
|
||||
|
||||
## Scope & constraints
|
||||
|
||||
**In scope**
|
||||
|
||||
- <!-- bullets -->
|
||||
|
||||
**Explicitly out of scope / not done here**
|
||||
|
||||
- <!-- bullets — prevents reviewers assuming missing work is an oversight -->
|
||||
|
||||
## Implementation notes
|
||||
|
||||
<!-- Optional: design choices, tradeoffs, follow-ups -->
|
||||
|
||||
## Testing & verification
|
||||
|
||||
<!-- What you ran; paste commands. Omit sections that do not apply. -->
|
||||
|
||||
- [ ] `cd gitnexus && npm test`
|
||||
- [ ] `cd gitnexus && npm run test:integration` *(if core/indexing/MCP paths changed)*
|
||||
- [ ] `cd gitnexus && npx tsc --noEmit`
|
||||
- [ ] `cd gitnexus-web && npm test` *(if web changed)*
|
||||
- [ ] `cd gitnexus-web && npx tsc -b --noEmit` *(if web changed)*
|
||||
- [ ] Manual / Playwright E2E *(note environment — see `gitnexus-web/e2e/`)*
|
||||
|
||||
## Risk & rollout
|
||||
|
||||
<!-- Breaking changes, migrations, index refresh (`npx gitnexus analyze`), release notes -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] PR body meets repo minimum length (workflow may label short descriptions)
|
||||
- [ ] If `AGENTS.md` / overlays changed: headers, scope block, and changelog updated per project conventions
|
||||
- [ ] No secrets, tokens, or machine-specific paths committed
|
||||
105
AGENTS.md
105
AGENTS.md
|
|
@ -1,7 +1,69 @@
|
|||
<!-- version: 1.2.0 -->
|
||||
<!--
|
||||
Metadata: version, last reviewed, scope, model policy, reference docs, changelog.
|
||||
Last updated: 2026-03-22
|
||||
-->
|
||||
|
||||
Last reviewed: 2026-03-24
|
||||
|
||||
**Project:** GitNexus · **Environment:** dev · **Maintainer:** repository maintainers (see GitHub)
|
||||
|
||||
This file uses a standard agent header (version, scope, model policy, reference docs, changelog), adapted for this **TypeScript/JavaScript monorepo**.
|
||||
|
||||
## Scope
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Reads** | Repository tree as needed for the task: `gitnexus/`, `gitnexus-web/`, `eval/`, plugin packages, `.github/`, `.gitnexus/` when present, and docs. |
|
||||
| **Writes** | Only paths required for the requested change; keep diffs minimal. Update lockfiles when dependencies change. |
|
||||
| **Executes** | `npm`, `npx`, `node` under `gitnexus/` and `gitnexus-web/`; `uv run` for Python under `eval/` when applicable; shell utilities for documented CI/dev workflows. |
|
||||
| **Off-limits** | User secrets (e.g. real `.env`), production deployment credentials, unrelated repositories, destructive git history operations without explicit human confirmation. |
|
||||
|
||||
## Model Configuration
|
||||
|
||||
- **Primary:** Pin in **Cursor** (Settings → model). Use a **named** model (e.g. GPT-5.2, Claude Sonnet 4.x). Avoid relying on **Auto** when reproducibility or audit trail matters.
|
||||
- **Fallback:** As configured in Cursor or your organization (do not encode `latest` or wildcards in automation configs).
|
||||
- **Notes:** The open-source GitNexus CLI indexer does not call an LLM. Optional Nexus AI in the web UI uses end-user provider keys and models.
|
||||
|
||||
## Execution Sequence (complex tasks)
|
||||
|
||||
Long sessions dilute instructions. For **multi-step** work, state up front:
|
||||
|
||||
1. Which rules in this file and **[GUARDRAILS.md](GUARDRAILS.md)** apply (and any relevant Signs).
|
||||
2. Current **Scope** boundaries (Reads / Writes / Off-limits).
|
||||
3. Which **validation commands** you will run (e.g. `cd gitnexus && npm test`, `npx tsc --noEmit`).
|
||||
|
||||
On very long threads, the human may add *“Remember: apply all AGENTS.md rules”* to re-weight rule tokens against context dilution.
|
||||
|
||||
## Claude Code hooks
|
||||
|
||||
Hooks enforce gates that prompts cannot. In **Claude Code**, **PreToolUse** hooks can block tools such as `git_commit` until checks pass. Adapt to this repo: e.g. `cd gitnexus && npm test` before commit.
|
||||
|
||||
## Context budget (Cursor / standards)
|
||||
|
||||
Generic “core standards” playbooks are often long and stack-specific. For this monorepo, commands and gotchas live under **Cursor Cloud specific instructions** below and in **[CONTRIBUTING.md](CONTRIBUTING.md)**. If always-on rules grow, split domain rules into **`.cursor/rules/*.mdc`** (globs). **Cursor:** project-wide rules live in **`.cursor/index.mdc`** (YAML frontmatter with `alwaysApply: true`). **Claude Code:** optionally load a **`STANDARDS.md`** only when needed (e.g. *“When writing new code, read STANDARDS.md”*) to save context.
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
- **This repository:** **[ARCHITECTURE.md](ARCHITECTURE.md)**, **[CONTRIBUTING.md](CONTRIBUTING.md)**, **[GUARDRAILS.md](GUARDRAILS.md)**.
|
||||
- **Cursor:** `.cursor/index.mdc` (always-on rules); optional `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` is deprecated — see `.cursor/index.mdc`.
|
||||
- **Optional local files:** `NOTES.md` (short vendor-neutral project snapshot). For handoffs, keep notes local (e.g., a scratch file outside the repo) rather than committing `HANDOFF.md`.
|
||||
- **GitNexus:** skills under `.claude/skills/gitnexus/`; machine-oriented rules in the `gitnexus:start` … `gitnexus:end` block below.
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Version | Change |
|
||||
|------|---------|--------|
|
||||
| 2026-03-24 | 1.2.0 | Fixed gitnexus:start block duplication (was inlined in Reference Docs bullet). |
|
||||
| 2026-03-23 | 1.1.0 | Updated agent instructions (sections, references, Cursor layout). |
|
||||
| 2026-03-22 | 1.0.0 | Added structured agent header and changelog. |
|
||||
|
||||
---
|
||||
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2487 symbols, 6056 relationships, 188 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus**. Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. For current symbol stats, run `npx gitnexus analyze` and inspect `.gitnexus/meta.json`.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
|
@ -99,3 +161,44 @@ To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.
|
|||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Repository structure
|
||||
|
||||
This is a monorepo with two main products and supporting config packages:
|
||||
|
||||
| Component | Path | Purpose |
|
||||
|-----------|------|---------|
|
||||
| **GitNexus CLI/Core** | `gitnexus/` | Main product — TypeScript CLI, indexing pipeline, MCP server. Published to npm. |
|
||||
| **GitNexus Web UI** | `gitnexus-web/` | React/Vite browser app — graph explorer + AI chat. Runs entirely in WASM. |
|
||||
| Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace (no build). |
|
||||
| Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor (no build). |
|
||||
| SWE-bench Eval | `eval/` | Python evaluation harness (optional; needs Docker + LLM API keys). |
|
||||
|
||||
### Running services
|
||||
|
||||
- **CLI/Core**: `cd gitnexus && npm run dev` (tsx watch mode) or `npm run build && node dist/cli/index.js <command>`
|
||||
- **Web UI**: `cd gitnexus-web && npm run dev` (Vite on port 5173)
|
||||
- **Backend mode**: `cd <indexed-repo> && node /workspace/gitnexus/dist/cli/index.js serve` (HTTP API on port 3741 by default)
|
||||
|
||||
### Testing
|
||||
|
||||
**CLI / Core (`gitnexus/`)**
|
||||
- **Unit tests**: `cd gitnexus && npm test` (vitest, ~2000 tests)
|
||||
- **Integration tests**: `cd gitnexus && npm run test:integration` (vitest, ~1850 tests). Two LadybugDB file-locking tests (`lbug-core-adapter`, `search-core`) may fail in containerized environments due to `/tmp` locking limitations — this is a known environment issue, not a code bug.
|
||||
- **TypeScript check**: `cd gitnexus && npx tsc --noEmit`
|
||||
|
||||
**Web UI (`gitnexus-web/`)**
|
||||
- **Unit tests**: `cd gitnexus-web && npm test` (vitest, ~200 tests)
|
||||
- **E2E tests**: `cd gitnexus-web && E2E=1 npx playwright test` (Playwright, 5 tests — requires `gitnexus serve` + `npm run dev` running)
|
||||
- **TypeScript check**: `cd gitnexus-web && npx tsc -b --noEmit`
|
||||
|
||||
No separate lint command is configured; TypeScript strict checking serves as the primary static analysis.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- `npm install` in `gitnexus/` triggers `prepare` (builds via `tsc`) and `postinstall` (patches tree-sitter-swift). Native tree-sitter bindings require `python3`, `make`, and `g++` to be present.
|
||||
- `tree-sitter-kotlin` and `tree-sitter-swift` are optional dependencies — install warnings for these are expected and non-blocking.
|
||||
- The Web UI uses `vite-plugin-wasm` and requires `Cross-Origin-Opener-Policy`/`Cross-Origin-Embedder-Policy` headers for `SharedArrayBuffer` (handled automatically by Vite dev server).
|
||||
- There is no ESLint/Prettier configuration in this repo.
|
||||
|
|
|
|||
66
ARCHITECTURE.md
Normal file
66
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Architecture — GitNexus
|
||||
|
||||
This repository is a **monorepo** with two main products: the **CLI / MCP package** (`gitnexus/`) and the **browser UI** (`gitnexus-web/`). Supporting folders ship editor integrations and plugins without changing the core graph engine.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `gitnexus/` | Published npm package `gitnexus`: CLI, MCP server (stdio), local HTTP API for bridge mode, ingestion pipeline, LadybugDB graph, embeddings (optional). |
|
||||
| `gitnexus-web/` | Vite + React UI: in-browser indexing (WASM), graph visualization, optional connection to `gitnexus serve`. |
|
||||
| `.claude/`, `gitnexus-claude-plugin/`, `gitnexus-cursor-integration/` | Packaged **skills** and plugin metadata so agents discover the same workflows as documented in `AGENTS.md`. |
|
||||
| `eval/` | Evaluation harnesses and docs for benchmarking tool usage. |
|
||||
| `.github/` | CI workflows (quality, unit, integration, E2E) and composite actions. |
|
||||
|
||||
## End-to-end flow: index → graph → tools
|
||||
|
||||
1. **Ingestion** (`gitnexus analyze`)
|
||||
- Entry: `gitnexus/src/cli/analyze.ts` → `runPipelineFromRepo` in `gitnexus/src/core/ingestion/pipeline.ts`.
|
||||
- Walks the git working tree, parses supported languages via **Tree-sitter**, resolves imports/calls/inheritance, detects **communities** and **processes** (execution flows), and builds an in-memory **knowledge graph** (`gitnexus/src/core/graph/`).
|
||||
- Output is loaded into **LadybugDB** under **`.gitnexus/`** at the repo root (`lbug/`, `meta.json`, etc.). Optional **FTS** indexes and **embeddings** attach to the same store.
|
||||
- The repo is registered in **`~/.gitnexus/registry.json`** so MCP can find it from any working directory.
|
||||
|
||||
2. **Persistence & metadata**
|
||||
- `gitnexus/src/storage/repo-manager.ts` — paths, registry, cleanup of legacy Kuzu artifacts.
|
||||
- `gitnexus/src/core/lbug/lbug-adapter.ts` — graph load, queries, embedding restore batches.
|
||||
|
||||
3. **Query & agents**
|
||||
- **MCP (stdio):** `gitnexus/src/cli/mcp.ts` → `startMCPServer` → `LocalBackend` (`gitnexus/src/mcp/local/local-backend.ts`) opens registered repos and serves **tools** from `gitnexus/src/mcp/tools.ts` and **resources** from `gitnexus/src/mcp/resources.ts`.
|
||||
- **Bridge HTTP:** `gitnexus/src/cli/serve.ts` → Express app in `gitnexus/src/server/api.ts` (CORS-limited) exposes REST + MCP-over-HTTP for the web UI.
|
||||
- **CLI tools (no MCP):** `gitnexus query`, `context`, `impact`, `cypher` in `gitnexus/src/cli/tool.ts` call the same backend for scripts and CI.
|
||||
|
||||
4. **Staleness**
|
||||
- `gitnexus/src/mcp/staleness.ts` compares indexed `lastCommit` to `HEAD` and surfaces hints when the graph is behind git.
|
||||
|
||||
## MCP tools (summary)
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `list_repos` | Discover indexed repositories when more than one is registered. |
|
||||
| `query` | Natural-language / keyword search over the graph (hybrid BM25 + optional vectors). |
|
||||
| `cypher` | Ad hoc **Cypher** against the schema (see resource `gitnexus://repo/{name}/schema`). |
|
||||
| `context` | Callers, callees, processes for one symbol (with disambiguation). |
|
||||
| `impact` | Blast radius (upstream/downstream) with depth and risk summary. |
|
||||
| `detect_changes` | Map git diffs to affected symbols and processes. |
|
||||
| `rename` | Graph-assisted rename with `dry_run` preview (`graph` vs `text_search` confidence). |
|
||||
|
||||
## Where to change what
|
||||
|
||||
| If you are changing… | Start in… |
|
||||
|----------------------|-----------|
|
||||
| CLI commands / flags | `gitnexus/src/cli/` (`index.ts`, per-command modules). |
|
||||
| Parsing or graph construction | `gitnexus/src/core/ingestion/` (pipeline, processors, resolvers, type-extractors). |
|
||||
| Graph schema / DB access | `gitnexus/src/core/lbug/` (`schema.ts`, `lbug-adapter.ts`), `gitnexus/src/mcp/core/lbug-adapter.ts` if MCP-specific. |
|
||||
| MCP protocol, tools, resources | `gitnexus/src/mcp/server.ts`, `tools.ts`, `resources.ts`. |
|
||||
| Search ranking | `gitnexus/src/core/search/` (BM25, hybrid fusion). |
|
||||
| Embeddings | `gitnexus/src/core/embeddings/`, phases in `analyze.ts`. |
|
||||
| Wiki generation | `gitnexus/src/core/wiki/`. |
|
||||
| Web UI behavior | `gitnexus-web/src/` (components, workers, graph client). |
|
||||
| CI | `.github/workflows/*.yml`, `.github/actions/setup-gitnexus/`. |
|
||||
|
||||
## Related docs
|
||||
|
||||
- [RUNBOOK.md](RUNBOOK.md) — operational commands and recovery.
|
||||
- [GUARDRAILS.md](GUARDRAILS.md) — safety boundaries for humans and agents.
|
||||
- [TESTING.md](TESTING.md) — how to run tests.
|
||||
- `AGENTS.md` / `CLAUDE.md` — agent workflows and tool usage expectations for **this** repo when indexed by GitNexus.
|
||||
113
CLAUDE.md
113
CLAUDE.md
|
|
@ -1,101 +1,52 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
<!-- version: 1.2.0 -->
|
||||
<!--
|
||||
Metadata: version, last reviewed, scope, model policy, reference docs, changelog.
|
||||
Last updated: 2026-03-22
|
||||
-->
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2487 symbols, 6056 relationships, 188 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
Last reviewed: 2026-03-24
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
**Project:** GitNexus · **Environment:** dev · **Maintainer:** repository maintainers (see GitHub)
|
||||
|
||||
## Always Do
|
||||
Follow **AGENTS.md** for the canonical rules; this file adds Claude Code–specific deltas. Cursor-specific notes live only in `AGENTS.md`.
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
## Scope
|
||||
|
||||
## When Debugging
|
||||
See the **Scope** table in [AGENTS.md](AGENTS.md) for read/write/execute/off-limits boundaries. Cursor-specific workflow notes also live only in AGENTS.md.
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/GitNexus/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
## Model Configuration
|
||||
|
||||
## When Refactoring
|
||||
- **Primary:** Pin per **Claude Code** / Anthropic org policy (explicit model id). Do not rely on an unversioned `latest` alias for governed workflows.
|
||||
- **Fallback:** As configured in Claude Code (organization default or user override).
|
||||
- **Notes:** The GitNexus CLI analyzer does not call an LLM.
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
## Execution Sequence (complex tasks)
|
||||
|
||||
## Never Do
|
||||
Same discipline as [AGENTS.md](AGENTS.md): before large multi-step work, state which **AGENTS.md** / **GUARDRAILS.md** rules apply, current **Scope**, and planned validation commands (`npm test`, `tsc`, etc.). When pausing, summarize progress in the chat or a **local** scratch file (do not add `HANDOFF.md` to the repo), then `/clear` and resume with that summary.
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
## Claude Code hooks
|
||||
|
||||
## Tools Quick Reference
|
||||
Prefer **PreToolUse** hooks for hard gates (e.g. tests before `git_commit`). Adapt hook commands to `gitnexus/` npm scripts.
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
## Context budget
|
||||
|
||||
## Impact Risk Levels
|
||||
If always-on instructions grow, load deep conventions via conditional reads (e.g. *“When writing new code, read STANDARDS.md”*) instead of pasting long blocks here. In Cursor, prefer `.cursor/index.mdc` plus optional `.cursor/rules/*.mdc` globs (see [AGENTS.md](AGENTS.md) § Context budget).
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
## Reference Documentation
|
||||
|
||||
## Resources
|
||||
- **This repository:** [AGENTS.md](AGENTS.md) (Cursor + monorepo notes), [ARCHITECTURE.md](ARCHITECTURE.md), [CONTRIBUTING.md](CONTRIBUTING.md), [GUARDRAILS.md](GUARDRAILS.md).
|
||||
- **GitNexus:** `.claude/skills/gitnexus/`; MCP and indexed-repo rules live only in [AGENTS.md](AGENTS.md) (`gitnexus:start` … `gitnexus:end`). See **GitNexus rules** below.
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/GitNexus/clusters` | All functional areas |
|
||||
| `gitnexus://repo/GitNexus/processes` | All execution flows |
|
||||
| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace |
|
||||
## Changelog
|
||||
|
||||
## Self-Check Before Finishing
|
||||
| Date | Version | Change |
|
||||
|------|---------|--------|
|
||||
| 2026-03-24 | 1.2.0 | Removed duplicated gitnexus:start block and scope table; replaced with pointers to AGENTS.md. |
|
||||
| 2026-03-23 | 1.1.0 | Updated agent instructions to match AGENTS.md. |
|
||||
| 2026-03-22 | 1.0.0 | Added structured header and changelog. |
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
---
|
||||
|
||||
## Keeping the Index Fresh
|
||||
## GitNexus rules
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
GitNexus MCP rules are in the `<!-- gitnexus:start -->` … `<!-- gitnexus:end -->` block in **[AGENTS.md](AGENTS.md)** — load that section when working with MCP tools or the graph index.
|
||||
|
|
|
|||
50
CONTRIBUTING.md
Normal file
50
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Contributing to GitNexus
|
||||
|
||||
How to propose changes, run checks locally, and open pull requests.
|
||||
|
||||
## License
|
||||
|
||||
This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/). By contributing, you agree your contributions are licensed under the same terms unless stated otherwise.
|
||||
|
||||
## Where to discuss
|
||||
|
||||
- **Issues & feature ideas:** use [GitHub Issues](https://github.com/abhigyanpatwari/GitNexus/issues) for the upstream repo, or your fork’s tracker if you work from a fork.
|
||||
- **Community:** see the Discord link in the root [README.md](README.md).
|
||||
|
||||
## Development setup
|
||||
|
||||
1. Clone the repository.
|
||||
2. **CLI / MCP package:** `cd gitnexus && npm install && npm run build`
|
||||
3. **Web UI (if needed):** `cd gitnexus-web && npm install`
|
||||
4. Run tests as described in [TESTING.md](TESTING.md).
|
||||
|
||||
## Branch and pull requests
|
||||
|
||||
- Use short-lived branches off the default branch of the repo you are targeting.
|
||||
- Prefer **conventional commits** (short prefix + description), for example:
|
||||
|
||||
```text
|
||||
feat: add graph export option
|
||||
fix: correct MCP tool schema for query
|
||||
test: cover cluster merge edge case
|
||||
docs: clarify analyze flags
|
||||
```
|
||||
|
||||
- **PR title:** `[area] Short description` (e.g. `[cli] Fix index refresh race`).
|
||||
- **PR description:** what changed, why, how to verify (commands), and any risk or rollback notes.
|
||||
|
||||
## Before you open a PR
|
||||
|
||||
- [ ] Tests pass for the packages you touched (`gitnexus` and/or `gitnexus-web`).
|
||||
- [ ] Typecheck passes: `npx tsc --noEmit` in `gitnexus/` and `npx tsc -b --noEmit` in `gitnexus-web/`.
|
||||
- [ ] No secrets, tokens, or machine-specific paths committed.
|
||||
- [ ] Documentation updated if behavior or public CLI/MCP contract changes.
|
||||
- [ ] Pre-commit hook runs clean (`.husky/pre-commit` — typecheck + unit tests for staged packages).
|
||||
|
||||
## Code review
|
||||
|
||||
Maintainers may request changes for correctness, tests, performance, or consistency with existing patterns. Keeping diffs focused makes review faster.
|
||||
|
||||
## AI-assisted contributions
|
||||
|
||||
If you use coding agents, follow project context files (e.g. `AGENTS.md`, `CLAUDE.md`) and avoid drive-by refactors unrelated to the issue. Prefer incremental, test-backed changes.
|
||||
88
GUARDRAILS.md
Normal file
88
GUARDRAILS.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Guardrails — GitNexus (repo + agents)
|
||||
|
||||
Rules for **human contributors** and **AI agents** working on this codebase or publishing artifacts. These complement `AGENTS.md` / `CLAUDE.md` (which focus on GitNexus-in-GitNexus workflows).
|
||||
|
||||
## Scope (typical agent session)
|
||||
|
||||
When automating changes in this repository, treat scope as **least privilege**:
|
||||
|
||||
- **Read:** Source, tests, docs, public config as needed for the task.
|
||||
- **Write:** Only files required for the requested fix or feature; avoid unrelated formatting or refactors.
|
||||
- **Execute:** Tests, typecheck, and documented CLI commands; do not run destructive commands on user data outside the repo without explicit approval.
|
||||
- **Off-limits:** Other people’s machines, production deployments you don’t own, and credentials you didn’t receive permission to use.
|
||||
|
||||
Adjust explicitly if the maintainer defines a different scope for a task.
|
||||
|
||||
---
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
1. **Never commit secrets** — API keys, tokens, `.env` with real values, private URLs, or session cookies. Use `.env.example` with placeholders only.
|
||||
2. **Never rename symbols with blind find-and-replace** when working in a GitNexus-indexed project — use the **`rename` MCP tool** with **`dry_run: true` first**, then review `graph` vs `text_search` edits. (There is no separate `gitnexus rename` CLI; renaming goes through MCP or editor integration.)
|
||||
3. **Run impact analysis before editing shared symbols** — use **`impact`** (upstream) for functions/classes/methods others call; do not ignore **HIGH** / **CRITICAL** risk without maintainer sign-off.
|
||||
4. **Prefer `detect_changes` before commit** — confirm diffs map to expected symbols/processes when the graph is available.
|
||||
5. **Preserve embeddings** — if `.gitnexus/meta.json` shows embeddings, run `npx gitnexus analyze --embeddings` when refreshing the index; plain `analyze` can drop them.
|
||||
|
||||
---
|
||||
|
||||
## Signs (recurring failure patterns)
|
||||
|
||||
Use this format: **Trigger → Instruction → Reason**.
|
||||
Append new Signs here when the same mistake repeats (e.g. CI broken twice the same way).
|
||||
|
||||
### Sign: Stale graph after edits
|
||||
|
||||
- **Trigger:** MCP or resources warn the index is behind `HEAD`, or code search doesn’t match latest commit.
|
||||
- **Instruction:** Run `npx gitnexus analyze` from the repo root (plus `--embeddings` if the project used them).
|
||||
- **Reason:** Tools query LadybugDB built at last analyze; git changes are invisible until re-indexed.
|
||||
|
||||
### Sign: Embeddings vanished after analyze
|
||||
|
||||
- **Trigger:** Semantic search quality drops; `stats.embeddings` in `.gitnexus/meta.json` is 0 after a refresh.
|
||||
- **Instruction:** Re-run `npx gitnexus analyze --embeddings` and confirm `meta.json` reflects stored embeddings.
|
||||
- **Reason:** Embedding generation is opt-in; analyze without the flag does not preserve prior vectors.
|
||||
|
||||
### Sign: MCP lists no repos
|
||||
|
||||
- **Trigger:** MCP stderr says no indexed repos.
|
||||
- **Instruction:** Run `npx gitnexus analyze` in the target repository; verify `npx gitnexus list` shows it.
|
||||
- **Reason:** The MCP server discovers repos via `~/.gitnexus/registry.json`, populated by analyze.
|
||||
|
||||
### Sign: Wrong repo in multi-repo setups
|
||||
|
||||
- **Trigger:** Query/impact results clearly belong to another project.
|
||||
- **Instruction:** Call `list_repos`, then pass **`repo`** on subsequent tools (or use per-workspace MCP config).
|
||||
- **Reason:** Default target may be ambiguous when multiple repos are registered.
|
||||
|
||||
### Sign: LadybugDB lock / “database busy”
|
||||
|
||||
- **Trigger:** Errors opening `.gitnexus/lbug` while MCP and analyze both run.
|
||||
- **Instruction:** Stop overlapping processes; one writer at a time. Retry analyze or restart MCP.
|
||||
- **Reason:** Embedded DB expects single-process ownership of the store.
|
||||
|
||||
---
|
||||
|
||||
## Publishing & supply chain
|
||||
|
||||
- **npm:** Do not publish from unreviewed automation; follow maintainer release process. Bump version intentionally; tag releases to match `package.json`.
|
||||
- **Dependencies:** Prefer minimal, auditable changes to `package.json`; run tests and CI after lockfile updates.
|
||||
- **License:** This project ships under **PolyForm Noncommercial 1.0.0** — do not relicense or imply a different license in docs or metadata without maintainer approval.
|
||||
|
||||
---
|
||||
|
||||
## Escalation
|
||||
|
||||
Stop and ask a **human maintainer** when:
|
||||
|
||||
- Impact analysis shows **HIGH** / **CRITICAL** risk and the task still requires the change.
|
||||
- You need to alter **CI**, **release**, or **security-sensitive** config.
|
||||
- Requirements conflict (e.g. “speed up analyze” vs “must keep all embeddings on huge repo”).
|
||||
- You are unsure whether data loss is acceptable (`clean`, forced migrations, schema changes).
|
||||
|
||||
---
|
||||
|
||||
## Related docs
|
||||
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — components and data flow.
|
||||
- [RUNBOOK.md](RUNBOOK.md) — commands for recovery.
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — PR and commit expectations.
|
||||
|
|
@ -59,6 +59,14 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
|
|||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — packages, index → graph → MCP flow, where to change code
|
||||
- [RUNBOOK.md](RUNBOOK.md) — analyze, embeddings, stale index, MCP recovery, CI snippets
|
||||
- [GUARDRAILS.md](GUARDRAILS.md) — safety rules and operational “Signs” for contributors and agents
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — license, setup, commits, and pull requests
|
||||
- [TESTING.md](TESTING.md) — test commands for `gitnexus` and `gitnexus-web`
|
||||
|
||||
## CLI + MCP (recommended)
|
||||
|
||||
The CLI indexes your repository and runs an MCP server that gives AI agents deep codebase awareness.
|
||||
|
|
|
|||
163
RUNBOOK.md
Normal file
163
RUNBOOK.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Runbook — GitNexus
|
||||
|
||||
Short, copy-paste operations for **local development**, **MCP**, and **CI**. Commands assume a Unix shell; on Windows use Git Bash or equivalent paths.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** ≥ 20 (`gitnexus-web/package.json` `engines`).
|
||||
- **Git** (analyze requires a git repository).
|
||||
- From repo root, install and build the CLI package:
|
||||
|
||||
```bash
|
||||
cd gitnexus
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Use `npx gitnexus …` from any path after global/published install, or `node dist/cli/index.js …` when developing from `gitnexus/` with a local build.
|
||||
|
||||
---
|
||||
|
||||
## Index out of date / “stale” tools
|
||||
|
||||
**Symptom:** MCP or resources warn the index is behind `HEAD`, or results don’t reflect recent commits.
|
||||
|
||||
**Fix (from the target repo root):**
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
**Force full rebuild** (same commit but suspect corruption or changed ignore rules):
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --force
|
||||
```
|
||||
|
||||
**Check status:**
|
||||
|
||||
```bash
|
||||
npx gitnexus status
|
||||
```
|
||||
|
||||
**List what MCP knows about:**
|
||||
|
||||
```bash
|
||||
npx gitnexus list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embeddings
|
||||
|
||||
**First time with vectors** (slower, more disk/RAM):
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
**Important:** If you already had embeddings, **always** pass `--embeddings` on later analyzes, or they can be dropped. See `stats.embeddings` in `.gitnexus/meta.json` (0 means none).
|
||||
|
||||
**Large repos:** Analyze may skip or limit embedding work when node counts are very high; watch CLI output.
|
||||
|
||||
---
|
||||
|
||||
## MCP: no repos / empty tools
|
||||
|
||||
**Symptom:** `GitNexus: No indexed repos yet` on stderr when starting MCP.
|
||||
|
||||
**Fix:** In each project you want indexed:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
Restart the editor MCP session if needed. The server **refreshes the registry lazily**; new analyzes are picked up without necessarily reinstalling MCP.
|
||||
|
||||
**Symptom:** Wrong repo when multiple are indexed — pass `repo` on tools or use `list_repos` first.
|
||||
|
||||
---
|
||||
|
||||
## Clean slate (corrupt or huge `.gitnexus`)
|
||||
|
||||
**Current repo only** (prompts for confirmation):
|
||||
|
||||
```bash
|
||||
npx gitnexus clean
|
||||
```
|
||||
|
||||
**Skip confirmation:**
|
||||
|
||||
```bash
|
||||
npx gitnexus clean --force
|
||||
```
|
||||
|
||||
**All registered repos:**
|
||||
|
||||
```bash
|
||||
npx gitnexus clean --all --force
|
||||
```
|
||||
|
||||
Then re-run `npx gitnexus analyze` (and `--embeddings` if you need vectors).
|
||||
|
||||
---
|
||||
|
||||
## Local bridge for the web UI
|
||||
|
||||
```bash
|
||||
cd gitnexus
|
||||
npx gitnexus serve
|
||||
# default http://127.0.0.1:4747 — see serve --help for port/host
|
||||
```
|
||||
|
||||
Use when the browser UI should talk to **local** indexed repos instead of WASM-only mode.
|
||||
|
||||
---
|
||||
|
||||
## CLI equivalents of MCP tools
|
||||
|
||||
Useful for debugging without an editor:
|
||||
|
||||
```bash
|
||||
cd gitnexus
|
||||
npx gitnexus query "authentication flow" --repo MyRepo
|
||||
npx gitnexus context SomeSymbol --repo MyRepo
|
||||
npx gitnexus impact SomeSymbol --direction upstream --repo MyRepo
|
||||
npx gitnexus cypher "MATCH (n) RETURN count(n) LIMIT 1" --repo MyRepo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI failures (contributors)
|
||||
|
||||
Orchestrator: `.github/workflows/ci.yml`.
|
||||
|
||||
| Job | Typical local repro |
|
||||
|-----|---------------------|
|
||||
| **quality** | `cd gitnexus && npx tsc --noEmit` |
|
||||
| **unit-tests** | `cd gitnexus && npx vitest run test/unit` |
|
||||
| **integration** | `cd gitnexus && npx vitest run test/integration` (see workflow matrix for groups) |
|
||||
| **e2e** | Triggered when `gitnexus-web/` changes; `cd gitnexus-web && E2E=1 npx playwright test` (requires `gitnexus serve` + `npm run dev`) |
|
||||
|
||||
**Note:** Pushes that touch only certain markdown paths may be skipped by `paths-ignore` in CI — see workflow file for exact patterns.
|
||||
|
||||
---
|
||||
|
||||
## Memory / analyze crashes
|
||||
|
||||
Analyze re-execs Node with a **large old-space heap** when needed (`analyze.ts`). If you still OOM on huge repos, close other processes, avoid `--embeddings` for a first pass, or analyze a smaller path if supported by your workflow.
|
||||
|
||||
---
|
||||
|
||||
## LadybugDB / lock errors
|
||||
|
||||
Only one process should open a repo’s `.gitnexus/lbug` store at a time. If MCP and a second `analyze` run conflict, stop one process, then retry `analyze` or restart MCP.
|
||||
|
||||
---
|
||||
|
||||
## Where to dig deeper
|
||||
|
||||
- Architecture overview: [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- Agent safety rules: [GUARDRAILS.md](GUARDRAILS.md)
|
||||
- Tests: [TESTING.md](TESTING.md)
|
||||
95
TESTING.md
Normal file
95
TESTING.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Testing — GitNexus
|
||||
|
||||
How we structure tests and which commands to run locally and in CI.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Path | Runner | Notes |
|
||||
| -------------- | -------------- | -------- | ------------------------------ |
|
||||
| CLI + MCP core | `gitnexus/` | Vitest | Primary test surface in CI |
|
||||
| Web UI | `gitnexus-web/`| Vitest | Unit/component tests |
|
||||
| Web UI E2E | `gitnexus-web/`| Playwright | Run when changing UI flows |
|
||||
|
||||
## Commands (local)
|
||||
|
||||
From repository root, unless noted:
|
||||
|
||||
**`gitnexus` (CLI / library)**
|
||||
|
||||
```bash
|
||||
cd gitnexus
|
||||
npm install
|
||||
npm run build
|
||||
npm test # unit: vitest run test/unit
|
||||
npm run test:integration # integration suite
|
||||
npm run test:all
|
||||
npm run test:coverage
|
||||
npx tsc --noEmit # typecheck (matches CI)
|
||||
```
|
||||
|
||||
**`gitnexus-web`**
|
||||
|
||||
```bash
|
||||
cd gitnexus-web
|
||||
npm install
|
||||
npm test # unit tests (vitest)
|
||||
npx tsc -b --noEmit # typecheck (matches CI)
|
||||
npm run test:coverage
|
||||
npm run test:e2e # Playwright (requires gitnexus serve + npm run dev)
|
||||
```
|
||||
|
||||
## Pre-commit hook
|
||||
|
||||
A husky pre-commit hook (`.husky/pre-commit`) runs automatically on every `git commit`:
|
||||
|
||||
- **`gitnexus-web/` files staged** → `tsc -b --noEmit` + `vitest run`
|
||||
- **`gitnexus/` files staged** → `tsc --noEmit` + `vitest run --project default`
|
||||
|
||||
Skip with `git commit --no-verify` (use sparingly).
|
||||
|
||||
## Test categories
|
||||
|
||||
- **Unit** — Pure logic, parsers, graph/query helpers; fast; no network.
|
||||
- **Integration** — Real combinations (filesystem, MCP wiring, larger pipelines) as already organized under `gitnexus/test/integration`.
|
||||
- **Eval-style / golden sets** — For agent- or classification-style behavior, keep labeled inputs and expected outputs (JSON or table-driven tests) and run them in CI when relevant.
|
||||
- **E2E (web)** — Critical user paths only; prefer `data-testid` attributes for stable selectors. Tests run against real backend (`gitnexus serve`) and Vite dev server.
|
||||
|
||||
## Performance metrics (targets)
|
||||
|
||||
Set targets to match team expectations, then tune to this repo’s CI reality:
|
||||
|
||||
| Metric | Target (initial) | Notes |
|
||||
| ------------------- | ---------------- | ------------------------------------------ |
|
||||
| Unit coverage | Align with CI | CI runs Vitest with coverage in `gitnexus` |
|
||||
| Unit wall time | Fast PR feedback | Use `vitest run test/unit` for tight loop |
|
||||
| Integration duration| < few minutes | Guard heavy tests with env flags if needed |
|
||||
|
||||
## Regression testing
|
||||
|
||||
Re-run the full relevant suite when:
|
||||
|
||||
- Prompt or agent-behavior documentation changes (if tests encode behavior)
|
||||
- Model or embedding-related code paths change
|
||||
- Graph schema, query contracts, or MCP tool shapes change
|
||||
- Dependencies with parsing or runtime impact upgrade
|
||||
|
||||
## CI integration
|
||||
|
||||
GitHub Actions (`.github/workflows/ci.yml`) orchestrate:
|
||||
|
||||
- **`ci-quality.yml`** — `tsc --noEmit` for `gitnexus/` + `tsc -b --noEmit` for `gitnexus-web/`
|
||||
- **`ci-tests.yml`** — `vitest run` with coverage (ubuntu) + cross-platform (macOS, Windows)
|
||||
- **`ci-e2e.yml`** — Playwright E2E tests, gated on `gitnexus-web/**` changes
|
||||
|
||||
Local checks before pushing:
|
||||
|
||||
```bash
|
||||
cd gitnexus && npx tsc --noEmit && npm test
|
||||
cd ../gitnexus-web && npx tsc -b --noEmit && npm test
|
||||
```
|
||||
|
||||
Or rely on the pre-commit hook which runs these automatically for staged files.
|
||||
|
||||
## User acceptance / beta (optional)
|
||||
|
||||
For staged releases or UI betas: deploy to a staging environment, collect structured feedback, watch errors and latency, then iterate before a wider release.
|
||||
|
|
@ -50,6 +50,10 @@ All models are routed through **OpenRouter** by default, so a single `OPENROUTER
|
|||
docker pull swebench/sweb.eval.x86_64.django_1776_django-16527:latest
|
||||
```
|
||||
|
||||
### Debug logging
|
||||
|
||||
Set `GITNEXUS_EVAL_DEBUG=1` to include full Python tracebacks in run summaries and logs. By default, errors are sanitized to avoid leaking host paths or stack traces.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Debug a single instance
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import time
|
|||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from constants import AUGMENT_TIMEOUT_SECONDS
|
||||
from minisweagent import Environment, Model
|
||||
from minisweagent.agents.default import AgentConfig, DefaultAgent
|
||||
from tool_registry import BINARIES_BY_KEY, TOOL_METRIC_KEYS
|
||||
|
||||
logger = logging.getLogger("gitnexus_agent")
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ class GitNexusMode(str, Enum):
|
|||
class GitNexusAgentConfig(AgentConfig):
|
||||
"""Extended config for GitNexus evaluation agent."""
|
||||
gitnexus_mode: GitNexusMode = GitNexusMode.BASELINE
|
||||
augment_timeout: float = 5.0
|
||||
augment_timeout: float = AUGMENT_TIMEOUT_SECONDS
|
||||
augment_min_pattern_length: int = 3
|
||||
track_gitnexus_usage: bool = True
|
||||
|
||||
|
|
@ -152,16 +154,10 @@ class GitNexusAgent(DefaultAgent):
|
|||
"""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
|
||||
for key, binary in BINARIES_BY_KEY.items():
|
||||
if binary in command and key in self.gitnexus_metrics.tool_calls:
|
||||
self.gitnexus_metrics.tool_calls[key] += 1
|
||||
break
|
||||
|
||||
def serialize(self, *extra_dicts) -> dict:
|
||||
"""Serialize with GitNexus-specific metrics."""
|
||||
|
|
@ -180,13 +176,7 @@ 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.tool_calls: dict[str, int] = {key: 0 for key in TOOL_METRIC_KEYS}
|
||||
self.augmentation_calls: int = 0
|
||||
self.augmentation_hits: int = 0
|
||||
self.augmentation_errors: int = 0
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import typer
|
|||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from tool_registry import TOOL_METRIC_KEYS
|
||||
|
||||
logger = logging.getLogger("analyze_results")
|
||||
console = Console()
|
||||
app = typer.Typer(rich_markup_mode="rich", add_completion=False)
|
||||
|
|
@ -77,13 +79,20 @@ def load_run_results(results_dir: Path) -> dict[str, dict]:
|
|||
|
||||
|
||||
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]
|
||||
"""Parse 'model_mode' into (model, mode) using known suffixes."""
|
||||
# Match the longest known suffix first to avoid hyphen collisions in model names.
|
||||
known_modes = [
|
||||
"native_augment",
|
||||
"native",
|
||||
"baseline",
|
||||
"mcp",
|
||||
"augment",
|
||||
"full",
|
||||
]
|
||||
for mode in known_modes:
|
||||
suffix = f"_{mode}"
|
||||
if run_id.endswith(suffix):
|
||||
return run_id[: -len(suffix)], mode
|
||||
return run_id, "unknown"
|
||||
|
||||
|
||||
|
|
@ -266,12 +275,17 @@ def compare_modes(
|
|||
_, mode = parse_run_id(run_id)
|
||||
metrics[mode] = compute_metrics(run_data)
|
||||
|
||||
mode_order = [
|
||||
mode
|
||||
for mode in ["baseline", "native", "native_augment", "mcp", "augment", "full"]
|
||||
if mode in metrics
|
||||
] or sorted(metrics.keys())
|
||||
|
||||
# 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")
|
||||
for mode in mode_order:
|
||||
table.add_column(mode, justify="right")
|
||||
|
||||
rows = [
|
||||
("Instances", "n_instances", "d"),
|
||||
|
|
@ -288,7 +302,7 @@ def compare_modes(
|
|||
|
||||
for label, key, fmt in rows:
|
||||
values = []
|
||||
for mode in ["baseline", "mcp", "augment", "full"]:
|
||||
for mode in mode_order:
|
||||
if mode in metrics:
|
||||
v = metrics[mode].get(key, 0)
|
||||
if fmt == ".1%":
|
||||
|
|
@ -307,8 +321,8 @@ def compare_modes(
|
|||
baseline_calls = metrics["baseline"]["avg_api_calls"]
|
||||
|
||||
table.add_section()
|
||||
for mode in ["mcp", "augment", "full"]:
|
||||
if mode not in metrics:
|
||||
for mode in mode_order:
|
||||
if mode == "baseline":
|
||||
continue
|
||||
mode_cost = metrics[mode]["avg_cost"]
|
||||
mode_calls = metrics[mode]["avg_api_calls"]
|
||||
|
|
@ -340,10 +354,8 @@ def gitnexus_usage(
|
|||
|
||||
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")
|
||||
for key in TOOL_METRIC_KEYS:
|
||||
table.add_column(key, justify="right")
|
||||
table.add_column("Total", justify="right")
|
||||
table.add_column("Augment Hits", justify="right")
|
||||
|
||||
|
|
@ -353,7 +365,7 @@ def gitnexus_usage(
|
|||
continue
|
||||
|
||||
# Aggregate tool calls across trajectories
|
||||
tool_totals: dict[str, int] = {"query": 0, "context": 0, "impact": 0, "cypher": 0, "overview": 0}
|
||||
tool_totals: dict[str, int] = {key: 0 for key in TOOL_METRIC_KEYS}
|
||||
augment_hits = 0
|
||||
|
||||
for traj in run_data.get("trajectories", {}).values():
|
||||
|
|
@ -373,10 +385,7 @@ def gitnexus_usage(
|
|||
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(tool_totals.get(key, 0)) for key in TOOL_METRIC_KEYS],
|
||||
str(total),
|
||||
str(augment_hits),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ import time
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from constants import (
|
||||
MCP_FIND_GITNEXUS_FALLBACK_TIMEOUT_SECONDS,
|
||||
MCP_FIND_GITNEXUS_TIMEOUT_SECONDS,
|
||||
MCP_READ_TIMEOUT_SECONDS,
|
||||
MCP_STOP_WAIT_SECONDS,
|
||||
)
|
||||
from utils.errors import is_debug_enabled, log_safe_exception
|
||||
|
||||
logger = logging.getLogger("mcp_bridge")
|
||||
|
||||
|
||||
|
|
@ -78,7 +86,7 @@ class MCPBridge:
|
|||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start MCP bridge: {e}")
|
||||
log_safe_exception(logger, "Failed to start MCP bridge", e, include_debug=is_debug_enabled())
|
||||
self.stop()
|
||||
return False
|
||||
|
||||
|
|
@ -86,9 +94,14 @@ class MCPBridge:
|
|||
"""Stop the MCP server subprocess."""
|
||||
if self.process:
|
||||
try:
|
||||
self.process.stdin.close()
|
||||
if self.process.stdin:
|
||||
self.process.stdin.close()
|
||||
if self.process.stdout:
|
||||
self.process.stdout.close()
|
||||
if self.process.stderr:
|
||||
self.process.stderr.close()
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
self.process.wait(timeout=MCP_STOP_WAIT_SECONDS)
|
||||
except Exception:
|
||||
try:
|
||||
self.process.kill()
|
||||
|
|
@ -146,7 +159,9 @@ class MCPBridge:
|
|||
try:
|
||||
result = subprocess.run(
|
||||
[cmd, "gitnexus", "--version"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=MCP_FIND_GITNEXUS_TIMEOUT_SECONDS,
|
||||
cwd=self.repo_path,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
|
|
@ -158,7 +173,9 @@ class MCPBridge:
|
|||
try:
|
||||
result = subprocess.run(
|
||||
["gitnexus", "--version"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=MCP_FIND_GITNEXUS_FALLBACK_TIMEOUT_SECONDS,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return "gitnexus"
|
||||
|
|
@ -194,7 +211,7 @@ class MCPBridge:
|
|||
self.process.stdin.flush()
|
||||
|
||||
# Read response
|
||||
response = self._read_response(timeout=30)
|
||||
response = self._read_response(timeout=MCP_READ_TIMEOUT_SECONDS)
|
||||
if response and response.get("id") == request_id:
|
||||
if "error" in response:
|
||||
logger.error(f"MCP error: {response['error']}")
|
||||
|
|
@ -203,7 +220,7 @@ class MCPBridge:
|
|||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MCP request failed: {e}")
|
||||
log_safe_exception(logger, "MCP request failed", e, include_debug=is_debug_enabled())
|
||||
return None
|
||||
|
||||
def _send_notification(self, method: str, params: dict):
|
||||
|
|
@ -224,42 +241,67 @@ class MCPBridge:
|
|||
self.process.stdin.write(message.encode("utf-8"))
|
||||
self.process.stdin.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"MCP notification failed: {e}")
|
||||
log_safe_exception(logger, "MCP notification failed", e, include_debug=is_debug_enabled())
|
||||
|
||||
def _read_response(self, timeout: float = 30) -> dict | None:
|
||||
def _read_content_length(self, deadline: float) -> int | None:
|
||||
"""Read Content-Length header, returning the byte length or None."""
|
||||
if not self.process or not self.process.stdout:
|
||||
return None
|
||||
|
||||
header_line = b""
|
||||
while time.time() < deadline:
|
||||
byte = self.process.stdout.read(1)
|
||||
if not byte:
|
||||
return None
|
||||
header_line += byte
|
||||
if header_line.endswith(b"\r\n\r\n") or header_line.endswith(b"\n\n"):
|
||||
break
|
||||
|
||||
if not header_line:
|
||||
return None
|
||||
|
||||
header_str = header_line.decode("utf-8").strip()
|
||||
for line in header_str.split("\r\n"):
|
||||
if line.lower().startswith("content-length:"):
|
||||
try:
|
||||
return int(line.split(":", 1)[1].strip())
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def _read_body(self, content_length: int, deadline: float) -> bytes | None:
|
||||
"""Read a response body of the expected length before deadline."""
|
||||
if not self.process or not self.process.stdout:
|
||||
return None
|
||||
|
||||
remaining = content_length
|
||||
chunks: list[bytes] = []
|
||||
|
||||
while remaining > 0 and time.time() < deadline:
|
||||
chunk = self.process.stdout.read(remaining)
|
||||
if not chunk:
|
||||
return None
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
|
||||
if remaining > 0:
|
||||
return None
|
||||
|
||||
return b"".join(chunks)
|
||||
|
||||
def _read_response(self, timeout: float = MCP_READ_TIMEOUT_SECONDS) -> 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
|
||||
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
content_length = self._read_content_length(deadline)
|
||||
if content_length is None:
|
||||
continue
|
||||
|
||||
# Read body
|
||||
body = self.process.stdout.read(content_length)
|
||||
body = self._read_body(content_length, deadline)
|
||||
if not body:
|
||||
return None
|
||||
|
||||
|
|
@ -272,7 +314,7 @@ class MCPBridge:
|
|||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading MCP response: {e}")
|
||||
log_safe_exception(logger, "Error reading MCP response", e, include_debug=is_debug_enabled())
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
15
eval/constants.py
Normal file
15
eval/constants.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
DEBUG_ENV_VAR = "GITNEXUS_EVAL_DEBUG"
|
||||
|
||||
# GitNexus eval-server health checks
|
||||
EVAL_SERVER_HEALTH_RETRIES = 30
|
||||
EVAL_SERVER_HEALTH_INTERVAL_SECONDS = 0.5
|
||||
EVAL_SERVER_HEALTH_TIMEOUT_SECONDS = 3
|
||||
|
||||
# MCP bridge timeouts
|
||||
MCP_FIND_GITNEXUS_TIMEOUT_SECONDS = 15
|
||||
MCP_FIND_GITNEXUS_FALLBACK_TIMEOUT_SECONDS = 10
|
||||
MCP_READ_TIMEOUT_SECONDS = 30
|
||||
MCP_STOP_WAIT_SECONDS = 5
|
||||
|
||||
# Agent defaults
|
||||
AUGMENT_TIMEOUT_SECONDS = 5.0
|
||||
|
|
@ -26,72 +26,20 @@ import shutil
|
|||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from constants import (
|
||||
EVAL_SERVER_HEALTH_INTERVAL_SECONDS,
|
||||
EVAL_SERVER_HEALTH_RETRIES,
|
||||
EVAL_SERVER_HEALTH_TIMEOUT_SECONDS,
|
||||
)
|
||||
from minisweagent.environments.docker import DockerEnvironment
|
||||
from tool_registry import TOOL_SPECS, ToolScriptSpec
|
||||
from utils.errors import is_debug_enabled, log_safe_exception
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
@ -133,7 +81,13 @@ class GitNexusDockerEnvironment(DockerEnvironment):
|
|||
try:
|
||||
self._setup_gitnexus()
|
||||
except Exception as e:
|
||||
logger.warning(f"GitNexus setup failed, continuing without it: {e}")
|
||||
log_safe_exception(
|
||||
logger,
|
||||
"GitNexus setup failed, continuing without it",
|
||||
e,
|
||||
include_debug=is_debug_enabled(),
|
||||
level="warning",
|
||||
)
|
||||
self._gitnexus_ready = False
|
||||
|
||||
return result
|
||||
|
|
@ -222,27 +176,59 @@ class GitNexusDockerEnvironment(DockerEnvironment):
|
|||
"timeout": 5,
|
||||
})
|
||||
|
||||
# Wait for the server to be ready (up to 15s for KuzuDB init)
|
||||
for i in range(30):
|
||||
time.sleep(0.5)
|
||||
# Wait for the server to be ready (up to ~15s for KuzuDB init)
|
||||
for i in range(EVAL_SERVER_HEALTH_RETRIES):
|
||||
time.sleep(EVAL_SERVER_HEALTH_INTERVAL_SECONDS)
|
||||
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,
|
||||
"timeout": EVAL_SERVER_HEALTH_TIMEOUT_SECONDS,
|
||||
})
|
||||
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")
|
||||
logger.info(
|
||||
f"Eval-server ready after {(i + 1) * EVAL_SERVER_HEALTH_INTERVAL_SECONDS:.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"Eval-server didn't become ready in "
|
||||
f"{EVAL_SERVER_HEALTH_RETRIES * EVAL_SERVER_HEALTH_INTERVAL_SECONDS:.1f}s. "
|
||||
f"Tools will fall back to direct CLI.\n"
|
||||
f"Server log: {log_output.get('output', 'N/A')}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_tool_script(spec: ToolScriptSpec, port: str) -> str:
|
||||
"""
|
||||
Render a standalone bash script for a GitNexus tool.
|
||||
|
||||
Scripts call the eval-server fast path when an endpoint is present,
|
||||
and fall back to the CLI otherwise.
|
||||
"""
|
||||
lines = ["#!/bin/bash"]
|
||||
|
||||
if spec.endpoint:
|
||||
lines.append(f'PORT="${{GITNEXUS_EVAL_PORT:-{port}}}"')
|
||||
|
||||
if spec.header:
|
||||
lines.append(spec.header.strip())
|
||||
|
||||
if spec.payload_builder:
|
||||
lines.append(spec.payload_builder.strip())
|
||||
|
||||
if spec.endpoint:
|
||||
lines.append(
|
||||
f'result=$(curl -sf -X POST "http://127.0.0.1:${{PORT}}{spec.endpoint}" '
|
||||
'-H "Content-Type: application/json" -d "$payload" 2>/dev/null)'
|
||||
)
|
||||
lines.append('if [ $? -eq 0 ] && [ -n "$result" ]; then echo "$result"; exit 0; fi')
|
||||
|
||||
lines.append(spec.fallback.strip())
|
||||
return "\n".join(lines)
|
||||
|
||||
def _install_tools(self):
|
||||
"""
|
||||
Install standalone GitNexus tool scripts in /usr/local/bin/.
|
||||
|
|
@ -259,24 +245,20 @@ class GitNexusDockerEnvironment(DockerEnvironment):
|
|||
"""
|
||||
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()
|
||||
for spec in TOOL_SPECS.values():
|
||||
script_content = self._render_tool_script(spec, 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}",
|
||||
"command": (
|
||||
f"cat << 'GITNEXUS_SCRIPT_EOF' > /usr/local/bin/{spec.bin_name}\n"
|
||||
f"{script_content}\n"
|
||||
"GITNEXUS_SCRIPT_EOF\n"
|
||||
f"chmod +x /usr/local/bin/{spec.bin_name}"
|
||||
),
|
||||
"timeout": 5,
|
||||
})
|
||||
|
||||
logger.info(f"Installed {len(tools)} GitNexus tool scripts in /usr/local/bin/")
|
||||
logger.info(f"Installed {len(TOOL_SPECS)} GitNexus tool scripts in /usr/local/bin/")
|
||||
|
||||
def _get_repo_info(self) -> dict:
|
||||
"""Get repository identity info from the container."""
|
||||
|
|
@ -325,7 +307,13 @@ class GitNexusDockerEnvironment(DockerEnvironment):
|
|||
logger.info(f"Cached GitNexus index: {cache_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cache GitNexus index: {e}")
|
||||
log_safe_exception(
|
||||
logger,
|
||||
"Failed to cache GitNexus index",
|
||||
e,
|
||||
include_debug=is_debug_enabled(),
|
||||
level="warning",
|
||||
)
|
||||
if cache_path.exists():
|
||||
shutil.rmtree(cache_path, ignore_errors=True)
|
||||
|
||||
|
|
@ -361,7 +349,13 @@ class GitNexusDockerEnvironment(DockerEnvironment):
|
|||
logger.info("GitNexus index restored from cache")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to restore cache, re-indexing: {e}")
|
||||
log_safe_exception(
|
||||
logger,
|
||||
"Failed to restore cache, re-indexing",
|
||||
e,
|
||||
include_debug=is_debug_enabled(),
|
||||
level="warning",
|
||||
)
|
||||
self._index_repository()
|
||||
|
||||
def stop(self) -> dict:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ dependencies = [
|
|||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"ruff>=0.5.0",
|
||||
"hypothesis>=6.88.0",
|
||||
"coverage>=7.6.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
@ -31,8 +33,8 @@ requires = ["hatchling"]
|
|||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["agents", "environments", "analysis", "bridge"]
|
||||
extra-files = ["run_eval.py"]
|
||||
packages = ["agents", "environments", "analysis", "bridge", "utils"]
|
||||
extra-files = ["run_eval.py", "tool_registry.py", "constants.py"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
|
|
|||
110
eval/run_eval.py
110
eval/run_eval.py
|
|
@ -25,7 +25,6 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -36,6 +35,8 @@ from rich.console import Console
|
|||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
|
||||
from utils.errors import is_debug_enabled, log_safe_exception
|
||||
|
||||
# Load .env file from eval/ directory
|
||||
_env_file = Path(__file__).parent / ".env"
|
||||
if _env_file.exists():
|
||||
|
|
@ -138,6 +139,65 @@ def get_swebench_docker_image(instance: dict) -> str:
|
|||
return image_name
|
||||
|
||||
|
||||
def _build_model(config: dict):
|
||||
"""Construct the model from config."""
|
||||
from minisweagent.models import get_model
|
||||
|
||||
return get_model(config=config.get("model", {}))
|
||||
|
||||
|
||||
def _build_environment(config: dict, instance: dict):
|
||||
"""Construct the environment for the instance."""
|
||||
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 environments.gitnexus_docker import GitNexusDockerEnvironment
|
||||
|
||||
env_config["image"] = get_swebench_docker_image(instance)
|
||||
return GitNexusDockerEnvironment(**env_config)
|
||||
|
||||
from minisweagent.environments.docker import DockerEnvironment
|
||||
|
||||
return DockerEnvironment(image=get_swebench_docker_image(instance), **env_config)
|
||||
|
||||
|
||||
def _build_agent(config: dict, model, env, instance_dir: Path, instance_id: str):
|
||||
"""Construct the GitNexus agent with trajectory output configured."""
|
||||
from agents.gitnexus_agent import GitNexusAgent
|
||||
|
||||
agent_config = dict(config.get("agent", {}))
|
||||
agent_config.pop("agent_class", "eval.agents.gitnexus_agent.GitNexusAgent")
|
||||
traj_path = instance_dir / f"{instance_id}.traj.json"
|
||||
agent_config["output_path"] = traj_path
|
||||
return GitNexusAgent(model, env, **agent_config)
|
||||
|
||||
|
||||
def _extract_submission(env, info: dict, run_id: str) -> str:
|
||||
"""Pull the git diff patch from the container, falling back to the agent submission."""
|
||||
try:
|
||||
patch_output = env.execute({"command": "cd /testbed && git diff"})
|
||||
return patch_output.get("output", "").strip()
|
||||
except Exception as patch_err:
|
||||
logger.warning(f"[{run_id}] Failed to extract patch: {patch_err}")
|
||||
return info.get("submission", "")
|
||||
|
||||
|
||||
def _record_failure(run_id: str, instance_id: str, result: dict, error: Exception):
|
||||
sanitized = log_safe_exception(
|
||||
logger,
|
||||
f"[{run_id}] Error on {instance_id}",
|
||||
error,
|
||||
include_debug=is_debug_enabled(),
|
||||
)
|
||||
result["exit_status"] = sanitized["error_type"]
|
||||
result["error_type"] = sanitized["error_type"]
|
||||
result["error_message"] = sanitized["error_message"]
|
||||
result["error"] = sanitized["error_message"]
|
||||
if "error_detail_debug" in sanitized:
|
||||
result["error_detail_debug"] = sanitized["error_detail_debug"]
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: dict,
|
||||
config: dict,
|
||||
|
|
@ -149,8 +209,6 @@ def process_instance(
|
|||
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
|
||||
|
|
@ -168,31 +226,12 @@ def process_instance(
|
|||
}
|
||||
|
||||
agent = None
|
||||
env = 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 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 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)
|
||||
model = _build_model(config)
|
||||
env = _build_environment(config, instance)
|
||||
agent = _build_agent(config, model, env, instance_dir, instance_id)
|
||||
|
||||
# Run
|
||||
logger.info(f"[{run_id}] Starting {instance_id}")
|
||||
|
|
@ -204,18 +243,10 @@ def process_instance(
|
|||
result["gitnexus_metrics"] = agent.gitnexus_metrics.to_dict()
|
||||
|
||||
# Extract git diff patch from the container (SWE-bench needs the model_patch)
|
||||
try:
|
||||
patch_output = env.execute({"command": "cd /testbed && git diff"})
|
||||
result["submission"] = patch_output.get("output", "").strip()
|
||||
except Exception as patch_err:
|
||||
logger.warning(f"[{run_id}] Failed to extract patch: {patch_err}")
|
||||
result["submission"] = info.get("submission", "")
|
||||
result["submission"] = _extract_submission(env, info, run_id)
|
||||
|
||||
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()
|
||||
_record_failure(run_id, instance_id, result, e)
|
||||
|
||||
finally:
|
||||
if agent:
|
||||
|
|
@ -287,7 +318,12 @@ def run_configuration(
|
|||
results.append(future.result())
|
||||
except Exception as e:
|
||||
iid = futures[future]
|
||||
logger.error(f"[{run_id}] Uncaught error for {iid}: {e}")
|
||||
log_safe_exception(
|
||||
logger,
|
||||
f"[{run_id}] Uncaught error for {iid}",
|
||||
e,
|
||||
include_debug=is_debug_enabled(),
|
||||
)
|
||||
|
||||
# Save run summary
|
||||
summary = {
|
||||
|
|
|
|||
1
eval/tests/__init__.py
Normal file
1
eval/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the GitNexus eval harness."""
|
||||
6
eval/tests/conftest.py
Normal file
6
eval/tests/conftest.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
29
eval/tests/test_errors.py
Normal file
29
eval/tests/test_errors.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from utils.errors import sanitize_exception
|
||||
|
||||
|
||||
def _raise_value_error():
|
||||
raise ValueError("boom")
|
||||
|
||||
|
||||
def test_sanitize_exception_without_debug(monkeypatch):
|
||||
monkeypatch.delenv("GITNEXUS_EVAL_DEBUG", raising=False)
|
||||
try:
|
||||
_raise_value_error()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
data = sanitize_exception(exc, include_debug=False)
|
||||
|
||||
assert data["error_type"] == "ValueError"
|
||||
assert data["error_message"] == "boom"
|
||||
assert "error_detail_debug" not in data
|
||||
|
||||
|
||||
def test_sanitize_exception_with_debug(monkeypatch):
|
||||
monkeypatch.setenv("GITNEXUS_EVAL_DEBUG", "1")
|
||||
try:
|
||||
_raise_value_error()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
data = sanitize_exception(exc)
|
||||
|
||||
assert data["error_type"] == "ValueError"
|
||||
assert "error_detail_debug" in data
|
||||
assert "ValueError" in data["error_detail_debug"]
|
||||
20
eval/tests/test_parse_run_id.py
Normal file
20
eval/tests/test_parse_run_id.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import pytest
|
||||
|
||||
analysis_module = pytest.importorskip("analysis.analyze_results")
|
||||
parse_run_id = analysis_module.parse_run_id
|
||||
|
||||
|
||||
def test_parse_run_id_native_augment():
|
||||
model, mode = parse_run_id("claude-sonnet_native_augment")
|
||||
assert model == "claude-sonnet"
|
||||
assert mode == "native_augment"
|
||||
|
||||
|
||||
def test_parse_run_id_hyphenated_model():
|
||||
model, mode = parse_run_id("glm-4.7_native")
|
||||
assert model == "glm-4.7"
|
||||
assert mode == "native"
|
||||
|
||||
|
||||
def test_parse_run_id_unknown():
|
||||
assert parse_run_id("custom_model") == ("custom_model", "unknown")
|
||||
63
eval/tests/test_property_based.py
Normal file
63
eval/tests/test_property_based.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from analysis.analyze_results import parse_run_id
|
||||
from environments.gitnexus_docker import GitNexusDockerEnvironment
|
||||
from tool_registry import TOOL_SPECS
|
||||
from utils.errors import sanitize_exception
|
||||
|
||||
|
||||
KNOWN_MODES = [
|
||||
"native_augment",
|
||||
"native",
|
||||
"baseline",
|
||||
"mcp",
|
||||
"augment",
|
||||
"full",
|
||||
]
|
||||
|
||||
|
||||
def _model_strategy():
|
||||
base_chars = st.characters(
|
||||
blacklist_categories=("Cs",),
|
||||
blacklist_characters={" ", "\n", "\t"},
|
||||
)
|
||||
text = st.text(alphabet=base_chars, min_size=1)
|
||||
return text.filter(lambda s: not any(s.endswith(f"_{m}") for m in KNOWN_MODES))
|
||||
|
||||
|
||||
@given(_model_strategy(), st.sampled_from(KNOWN_MODES))
|
||||
def test_parse_run_id_round_trip(model: str, mode: str) -> None:
|
||||
run_id = f"{model}_{mode}"
|
||||
parsed_model, parsed_mode = parse_run_id(run_id)
|
||||
assert parsed_model == model
|
||||
assert parsed_mode == mode
|
||||
|
||||
|
||||
@given(st.text())
|
||||
def test_sanitize_exception_respects_debug_flag(message: str) -> None:
|
||||
exc = ValueError(message)
|
||||
data = sanitize_exception(exc, include_debug=False)
|
||||
assert data["error_type"] == "ValueError"
|
||||
assert data["error_message"] == (message or "ValueError")
|
||||
assert "error_detail_debug" not in data
|
||||
|
||||
data_debug = sanitize_exception(exc, include_debug=True)
|
||||
assert data_debug["error_type"] == "ValueError"
|
||||
assert "error_detail_debug" in data_debug
|
||||
assert data_debug["error_detail_debug"]
|
||||
|
||||
|
||||
@given(st.sampled_from(list(TOOL_SPECS.values())), st.integers(min_value=1, max_value=99999))
|
||||
def test_render_tool_script_contains_expected_paths(spec, port: int) -> None:
|
||||
script = GitNexusDockerEnvironment._render_tool_script(spec, str(port))
|
||||
|
||||
assert spec.fallback.strip() in script
|
||||
if spec.endpoint:
|
||||
assert spec.endpoint in script
|
||||
assert f"${{GITNEXUS_EVAL_PORT:-{port}}}" in script
|
||||
assert "curl" in script
|
||||
else:
|
||||
assert "curl" not in script
|
||||
21
eval/tests/test_tool_scripts.py
Normal file
21
eval/tests/test_tool_scripts.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import pytest
|
||||
|
||||
GitNexusDockerEnvironment = pytest.importorskip(
|
||||
"environments.gitnexus_docker"
|
||||
).GitNexusDockerEnvironment
|
||||
tool_registry = pytest.importorskip("tool_registry")
|
||||
TOOL_SPECS = tool_registry.TOOL_SPECS
|
||||
|
||||
|
||||
def test_render_query_script_uses_endpoint_and_fallback():
|
||||
script = GitNexusDockerEnvironment._render_tool_script(TOOL_SPECS["query"], "4848")
|
||||
assert "/tool/query" in script
|
||||
assert "gitnexus query" in script
|
||||
assert "GITNEXUS_EVAL_PORT" in script
|
||||
|
||||
|
||||
def test_render_augment_script_skips_curl():
|
||||
script = GitNexusDockerEnvironment._render_tool_script(TOOL_SPECS["augment"], "4848")
|
||||
assert "/tool/" not in script
|
||||
assert "curl" not in script
|
||||
assert "gitnexus augment" in script
|
||||
79
eval/tool_registry.py
Normal file
79
eval/tool_registry.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolScriptSpec:
|
||||
key: str
|
||||
bin_name: str
|
||||
endpoint: str | None
|
||||
payload_builder: str
|
||||
fallback: str
|
||||
header: str | None = None
|
||||
|
||||
|
||||
TOOL_METRIC_KEYS: Tuple[str, ...] = ("query", "context", "impact", "cypher", "overview")
|
||||
|
||||
TOOL_SPECS: Dict[str, ToolScriptSpec] = {
|
||||
"query": ToolScriptSpec(
|
||||
key="query",
|
||||
bin_name="gitnexus-query",
|
||||
endpoint="/tool/query",
|
||||
payload_builder=r'''query="$1"; task_ctx="${2:-}"; goal="${3:-}"
|
||||
[ -z "$query" ] && echo "Usage: gitnexus-query <query> [task_context] [goal]" && exit 1
|
||||
payload="{\"query\": \"$query\""
|
||||
[ -n "$task_ctx" ] && payload="$payload, \"task_context\": \"$task_ctx\""
|
||||
[ -n "$goal" ] && payload="$payload, \"goal\": \"$goal\""
|
||||
payload="$payload}"''',
|
||||
fallback='cd /testbed && npx gitnexus query "$query" 2>&1',
|
||||
),
|
||||
"context": ToolScriptSpec(
|
||||
key="context",
|
||||
bin_name="gitnexus-context",
|
||||
endpoint="/tool/context",
|
||||
payload_builder=r'''name="$1"; file_path="${2:-}"
|
||||
[ -z "$name" ] && echo "Usage: gitnexus-context <symbol_name> [file_path]" && exit 1
|
||||
payload="{\"name\": \"$name\""
|
||||
[ -n "$file_path" ] && payload="$payload, \"file_path\": \"$file_path\""
|
||||
payload="$payload}"''',
|
||||
fallback='cd /testbed && npx gitnexus context "$name" 2>&1',
|
||||
),
|
||||
"impact": ToolScriptSpec(
|
||||
key="impact",
|
||||
bin_name="gitnexus-impact",
|
||||
endpoint="/tool/impact",
|
||||
payload_builder=r'''target="$1"; direction="${2:-upstream}"
|
||||
[ -z "$target" ] && echo "Usage: gitnexus-impact <symbol_name> [upstream|downstream]" && exit 1
|
||||
payload="{\"target\": \"$target\", \"direction\": \"$direction\"}"''',
|
||||
fallback='cd /testbed && npx gitnexus impact "$target" --direction "$direction" 2>&1',
|
||||
),
|
||||
"cypher": ToolScriptSpec(
|
||||
key="cypher",
|
||||
bin_name="gitnexus-cypher",
|
||||
endpoint="/tool/cypher",
|
||||
payload_builder=r'''query="$1"
|
||||
[ -z "$query" ] && echo "Usage: gitnexus-cypher <cypher_query>" && exit 1
|
||||
payload="{\"query\": \"$query\"}"''',
|
||||
fallback='cd /testbed && npx gitnexus cypher "$query" 2>&1',
|
||||
),
|
||||
"overview": ToolScriptSpec(
|
||||
key="overview",
|
||||
bin_name="gitnexus-overview",
|
||||
endpoint="/tool/list_repos",
|
||||
header='echo "=== Code Knowledge Graph Overview ==="',
|
||||
payload_builder='payload="{}"',
|
||||
fallback='cd /testbed && npx gitnexus list 2>&1',
|
||||
),
|
||||
"augment": ToolScriptSpec(
|
||||
key="augment",
|
||||
bin_name="gitnexus-augment",
|
||||
endpoint=None,
|
||||
payload_builder="",
|
||||
fallback='cd /testbed && npx gitnexus augment "$1" 2>&1 || true',
|
||||
),
|
||||
}
|
||||
|
||||
BINARIES_BY_KEY: Dict[str, str] = {spec.key: spec.bin_name for spec in TOOL_SPECS.values()}
|
||||
ENDPOINTS_BY_KEY: Dict[str, str | None] = {spec.key: spec.endpoint for spec in TOOL_SPECS.values()}
|
||||
1
eval/utils/__init__.py
Normal file
1
eval/utils/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Utility package for the eval harness."""
|
||||
60
eval/utils/errors.py
Normal file
60
eval/utils/errors.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from typing import Any, Callable
|
||||
|
||||
from constants import DEBUG_ENV_VAR
|
||||
|
||||
|
||||
def is_debug_enabled() -> bool:
|
||||
"""Return True when debug output (full tracebacks) should be emitted."""
|
||||
return os.getenv(DEBUG_ENV_VAR, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def sanitize_exception(exc: BaseException, *, include_debug: bool | None = None) -> dict[str, str]:
|
||||
"""
|
||||
Produce a log-safe, JSON-friendly view of an exception.
|
||||
|
||||
- Always returns error_type and error_message.
|
||||
- Only includes error_detail_debug (full traceback) when debug is enabled.
|
||||
"""
|
||||
debug = is_debug_enabled() if include_debug is None else include_debug
|
||||
|
||||
error_type = type(exc).__name__
|
||||
message = str(exc) or error_type
|
||||
|
||||
data: dict[str, str] = {
|
||||
"error_type": error_type,
|
||||
"error_message": message,
|
||||
}
|
||||
|
||||
if debug:
|
||||
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
||||
if tb:
|
||||
data["error_detail_debug"] = tb
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def log_safe_exception(
|
||||
logger: Any,
|
||||
prefix: str,
|
||||
exc: BaseException,
|
||||
*,
|
||||
include_debug: bool | None = None,
|
||||
level: str = "error",
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Log an exception without leaking stack traces unless debug is enabled.
|
||||
|
||||
Returns the sanitized dict so callers can persist it.
|
||||
"""
|
||||
data = sanitize_exception(exc, include_debug=include_debug)
|
||||
debug = "error_detail_debug" in data
|
||||
|
||||
log_fn: Callable[..., None] = getattr(logger, level, logger.error)
|
||||
message = f"{prefix}: {data['error_type']}: {data['error_message']}"
|
||||
log_kwargs = {"exc_info": True} if debug else {}
|
||||
log_fn(message, **log_kwargs)
|
||||
return data
|
||||
2529
eval/uv.lock
generated
Normal file
2529
eval/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
llms.txt
Normal file
20
llms.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
GitNexus monorepo (CLI + web UI)
|
||||
|
||||
# Core docs (read first for any contribution)
|
||||
- AGENTS.md — canonical agent instructions, GitNexus MCP tool reference
|
||||
- GUARDRAILS.md — non-negotiables and escalation triggers
|
||||
|
||||
# Operational (read when running commands or debugging)
|
||||
- RUNBOOK.md — copy-paste commands for common workflows
|
||||
- TESTING.md — test commands for both packages
|
||||
- CONTRIBUTING.md — contribution workflow and PR conventions
|
||||
|
||||
# Architecture (read for design context)
|
||||
- ARCHITECTURE.md — system overview, data flow, component relationships
|
||||
|
||||
# Key directories
|
||||
- gitnexus/ (CLI/core, MCP server — TypeScript)
|
||||
- gitnexus-web/ (web UI — React/Vite/WASM)
|
||||
- eval/ (SWE-bench evaluation harness — Python)
|
||||
- gitnexus-claude-plugin/ (Claude marketplace config)
|
||||
- gitnexus-cursor-integration/ (Cursor integration config)
|
||||
Loading…
Add table
Reference in a new issue