Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8772acc
Remove Zed runtime support and skills
ReidenXerx Jun 25, 2026
5b71ff9
Extract all guards to classify core + cursor adapter
ReidenXerx Jun 25, 2026
0f4ee01
Single-source prompt-router exts; fix api_impact contradiction
ReidenXerx Jun 25, 2026
240224f
Single-source the enforcement contract; add PDG/trace/explain
ReidenXerx Jun 25, 2026
634cd9c
Add Claude Code as a first-class runtime/adapter
ReidenXerx Jun 25, 2026
e446dd0
Relocate shared hook lib + state to neutral .gnkit; runtime-aware setup
ReidenXerx Jun 25, 2026
44708ea
migrate: clean legacy .cursor hook lib/config/state on upgrade
ReidenXerx Jun 25, 2026
0051171
Scrub deleted graph-session from manifests; add classify/emit libs
ReidenXerx Jun 25, 2026
24f7ae9
Teach full GitNexus tool surface; pre-commit full PDG refresh
ReidenXerx Jun 26, 2026
90998a6
Add 5 task-type skills (perf, feature-dev, testing, arch-review, laye…
ReidenXerx Jun 26, 2026
5783147
Autonomous agent-refresh now builds PDG (incremental + --pdg)
ReidenXerx Jun 26, 2026
6579f29
agent-refresh: full --force + PDG (match pre-commit, guarantee full P…
ReidenXerx Jul 1, 2026
e5b064a
Teach embedding technique: concept-phrase query, not keywords
ReidenXerx Jul 1, 2026
06f6ce3
Add persistent telemetry layer (gitnexus:stats)
ReidenXerx Jul 1, 2026
dcd8a9a
gitnexus:stats --json for dashboards
ReidenXerx Jul 1, 2026
7a5b829
Intel layer #1: gitnexus-microscope (microscope-waves deep audit)
ReidenXerx Jul 1, 2026
22f705b
Compaction middleware + durable memory layer
ReidenXerx Jul 3, 2026
176f390
Memory layer: use Claude Code's native project memory path (shared, n…
ReidenXerx Jul 3, 2026
13d0f49
Zed: write portable npx MCP command, never a hardcoded absolute path
ReidenXerx Jul 3, 2026
1b46798
Install cleans orphaned .zed gitnexus config when Zed not in runtime
ReidenXerx Jul 3, 2026
e698f54
Distribute skills via git: relocate store to tracked .gnkit/skills; r…
ReidenXerx Jul 3, 2026
aa51b20
Fix PreCompact hook: side-effect only (Claude rejects additionalConte…
ReidenXerx Jul 4, 2026
390ef46
Fix post-compaction regression: recovery brief dropped graph-first di…
ReidenXerx Jul 5, 2026
fce7ed6
Close two grep escape hatches: shell grep/rg + symbol alternation
ReidenXerx Jul 5, 2026
01b8e8e
Add classical-fallback escape hatch for when GitNexus is fresh-but-wrong
ReidenXerx Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@ node_modules/
*.tar.gz
eval/report.md
eval/BENCHMARK-realrepo.md
eval/swebench/results/*/trajectories/
eval/swebench/results/*/config.yaml

# Python build artifacts
__pycache__/
*.pyc
.venv/

# Local databases / graph indexes
*.db
*.sqlite
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# gitnexus-agent-kit

**The enforcement layer for GitNexus — Cursor, Zed, and Ollama**
**The enforcement layer for GitNexus — Cursor, Zed, Claude Code, and Ollama**

Hooks (Cursor) · Agent profiles (Zed) · MCP · Skills · Cypher · Autonomous refresh — graph-first reasoning on **every task**, not only when code is unfamiliar.

Expand Down Expand Up @@ -49,7 +49,11 @@ This kit closes that gap with **IDE-specific enforcement** — Cursor hooks, Zed
| --- | --- | --- |
| **Cursor** (`--runtime cursor`) | Hooks, rules, `.cursor/mcp.json`, skills | Hard — hooks deny grep/read when graph is fresh |
| **Zed** (`--runtime zed`) | `.zed/settings.json`, **Zed + GitNexus** agent profile, `.agents/skills/`, `AGENTS.md` | Profile — grep disabled; Zed model + gitnexus MCP |
| **Both** (`--runtime both`, default) | Everything above | Cursor hard gates + Zed profile for the same repo |
| **Claude Code** (`--runtime claude`) | `.mcp.json`, `.claude/settings.json` hooks, `.claude/skills/`, `CLAUDE.md` | Hard — PreToolUse hooks deny symbol grep / large read / blind edits; commit gated on `detect_changes` |
| **Both** (`--runtime both`, default) | Cursor + Zed | Cursor hard gates + Zed profile for the same repo |
| **All** (`--runtime all`) | Cursor + Zed + Claude Code | Every adapter (also any comma list, e.g. `--runtime cursor,claude`) |

Claude Code shares the **same enforcement core** as Cursor — the `classify.mjs` policy decides allow/deny, and a small `claude-emit` adapter maps the verdict to Claude Code's `PreToolUse` hook protocol. Same gates, same graph-first loop.

Skills live once in `.gitnexus/agent-kit/skills/` and are **symlinked** into `.cursor/skills/` and/or `.agents/skills/` — one source of truth, both IDEs stay in sync on update.

Expand Down
33 changes: 33 additions & 0 deletions bundle/.claude/hooks/gitnexus-bash-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Claude Code PreToolUse (Bash) → staleness gate, plus detect_changes-before-commit gate.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { classifyShell, classifyCommit } = await lib("classify.mjs");
const { gnContext, emitVerdict } = await lib("claude-emit.mjs");
const { isDetectUsed } = await lib("session-primer.mjs");

const ctx = gnContext(root);
const command = input.tool_input?.command ?? "";

let verdict = classifyShell({ command }, ctx);
if (verdict.decision === "allow") {
const commit = classifyCommit(
{ command },
{ ...ctx, detectUsed: isDetectUsed(root) },
);
if (commit.decision === "deny") verdict = commit;
}
emitVerdict(verdict, { root, mode: ctx.config.mode });
27 changes: 27 additions & 0 deletions bundle/.claude/hooks/gitnexus-edit-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
// Claude Code PreToolUse (Edit|Write|MultiEdit) → staleness + impact-before-edit gate.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { classifyEdit } = await lib("classify.mjs");
const { gnContext, emitVerdict } = await lib("claude-emit.mjs");
const { isImpactUsed } = await lib("session-primer.mjs");

const ctx = gnContext(root);
const verdict = classifyEdit(
{ toolInput: input.tool_input ?? {} },
{ ...ctx, impactUsed: isImpactUsed(root) },
);
emitVerdict(verdict, { root, mode: ctx.config.mode });
27 changes: 27 additions & 0 deletions bundle/.claude/hooks/gitnexus-grep-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
// Claude Code PreToolUse (Grep|Glob) → route symbol/field/broad searches to GitNexus.
// Thin glue over the shared classify core; Claude protocol mapping in claude-emit.mjs.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { classifyGrep } = await lib("classify.mjs");
const { gnContext, emitVerdict } = await lib("claude-emit.mjs");

const ctx = gnContext(root);
const verdict = classifyGrep(
{ tool: input.tool_name ?? "", toolInput: input.tool_input ?? {} },
ctx,
);
emitVerdict(verdict, { root, mode: ctx.config.mode });
37 changes: 37 additions & 0 deletions bundle/.claude/hooks/gitnexus-mcp-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
// Claude Code PreToolUse (mcp__gitnexus__*) → record graph usage; refresh-first when stale.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { gnContext, emitVerdict } = await lib("claude-emit.mjs");
const { setMcpToolUsed, bumpScore } = await lib("session-primer.mjs");

const ctx = gnContext(root);
const tool = input.tool_name ?? "";

if (ctx.phase === "must_refresh") {
emitVerdict(
{
decision: "deny",
agentMessage: ctx.staleMustRefreshMsg,
userKey: "stale.must_refresh",
},
{ root, mode: ctx.config.mode },
);
} else {
// Record the graph call so the impact/detect gates clear; then allow silently.
setMcpToolUsed(root, tool);
bumpScore(root, "graphCalls");
}
36 changes: 36 additions & 0 deletions bundle/.claude/hooks/gitnexus-precompact.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
// Claude Code PreCompact → SIDE-EFFECT ONLY: checkpoint durable state to memory + log the compaction.
//
// PreCompact CANNOT inject context: Claude Code allows hookSpecificOutput.additionalContext only on
// UserPromptSubmit / PostToolUse / Stop / SubagentStop — NOT PreCompact (emitting it errors the hook).
// There's also no agent turn between this hook and the compaction. So the "preserve everything /
// lose nothing" steering lands elsewhere: the always-on contract keeps the memory current, and the
// SessionStart(source:compact) recovery brief reconciles it afterward. This hook writes no stdout.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { gnContext } = await lib("claude-emit.mjs");
const { appendMemoryCheckpoint, isImpactUsed, isDetectUsed, bumpScore } =
await lib("session-primer.mjs");

const ctx = gnContext(root);
bumpScore(root, "compactions"); // surfaced in gitnexus:stats
appendMemoryCheckpoint(
root,
`- trigger: ${input.trigger || "auto"} | index: ${ctx.phase} | gates: impact ${isImpactUsed(root) ? "done" : "pending"}, detect_changes ${isDetectUsed(root) ? "done" : "pending"}\n` +
`- (transcript about to be summarized — task/decisions/open-items/file:line above must already be current)`,
);
// No stdout: PreCompact has no valid context-injection channel; steering is handled by the
// contract + SessionStart(compact) recovery.
43 changes: 43 additions & 0 deletions bundle/.claude/hooks/gitnexus-read-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node
// Claude Code PreToolUse (Read) → block large source reads; route to query/context.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { classifyRead } = await lib("classify.mjs");
const { gnContext, emitVerdict } = await lib("claude-emit.mjs");
const { readPromptHint } = await lib("session-primer.mjs");

const ti = input.tool_input ?? {};
const filePath = ti.file_path ?? ti.path ?? "";
const ctx = gnContext(root);
const verdict = classifyRead(
{ toolInput: ti },
{
...ctx,
promptHint: readPromptHint(root),
readLines: () => {
try {
const abs = path.resolve(root, filePath);
return fs.existsSync(abs)
? fs.readFileSync(abs, "utf8").split("\n").length
: 0;
} catch {
return 0;
}
},
},
);
emitVerdict(verdict, { root, mode: ctx.config.mode });
66 changes: 66 additions & 0 deletions bundle/.claude/hooks/gitnexus-session.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node
// Claude Code SessionStart → reset per-session gate flags and inject the GitNexus brief.
import path from "node:path";
import { pathToFileURL } from "node:url";

let raw = "";
for await (const c of process.stdin) raw += c;
let input = {};
try {
input = JSON.parse(raw || "{}");
} catch {
/* empty */
}
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
const lib = (rel) =>
import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);

const { existsSync } = await import("node:fs");
const { gnContext, emitContext } = await lib("claude-emit.mjs");
const {
clearSessionState,
shouldClearOnSource,
isImpactUsed,
isDetectUsed,
memoryPath,
fallbackGrant,
} = await lib("session-primer.mjs");

const source = input.source || "startup";
// compact | resume = the SAME task continuing → preserve gates + memory; don't re-arm.
const recovering = !shouldClearOnSource(source);
if (!recovering) clearSessionState(root);

const ctx = gnContext(root);
const mp = memoryPath(root); // Claude Code's native project memory
const grant = fallbackGrant(root);
const staleLine = grant
? `⚠ CLASSICAL FALLBACK active (${grant.reason || "GitNexus distrusted"}) — classical Grep/Read/shell allowed for ~${Math.max(1, Math.round(grant.remainingMs / 60000))} min. RE-CONFIRM findings with the graph once GitNexus is reliable; end early with \`npm run gitnexus:fallback:off\`.`
: ctx.phase !== "fresh"
? "Index is STALE — run `npm run gitnexus:agent-refresh` before graph calls (hooks block until refreshed)."
: "Index is fresh — hooks redirect symbol Grep / large Read / blind edits to the graph.";

let lines;
if (recovering) {
const hasMem = existsSync(memoryPath(root));
lines = [
`GitNexus: context was ${source === "compact" ? "COMPACTED" : "resumed"} — the task CONTINUES; enforcement and this session's satisfied gates are PRESERVED.`,
// Graph-first discipline MUST be re-stated here, not only on fresh start: post-compaction is
// exactly where agents drift back to grep/blind-read. "Gates preserved" ≠ "stop using the graph".
"Graph-first STILL applies — do NOT fall back to grep or blind Read: orient with gitnexus_query, drill with gitnexus_context, cypher for structure, impact before edits, detect_changes before commit.",
`Gates already satisfied: impact ${isImpactUsed(root) ? "✓ done" : "pending"}, detect_changes ${isDetectUsed(root) ? "✓ done" : "pending"} — don't redo those for work you ALREADY analyzed, but DO run impact before any NEW edit and detect_changes before every commit.`,
hasMem
? `RECOVER from your project memory (${mp}): reconcile it with reality NOW and fill gaps — decisions, requirements, open bugs, user intent, key file:line.`
: `Record the task state you still hold in your project memory (${mp}) — decisions, requirements, open items, key file:line — before continuing.`,
"NOTHING important from before the compaction may be lost — if the summary dropped a requirement/decision/finding, reconstruct it from your memory or the code before acting.",
staleLine,
];
} else {
lines = [
"GitNexus enforcement active (Claude Code). Graph-first on EVERY task — see CLAUDE.md.",
"Orient with gitnexus_query; drill with gitnexus_context; cypher for structure; impact before edits; detect_changes before commit.",
`Keep your project memory current as you work (${mp}) — it survives compaction + sessions; the transcript does not.`,
staleLine,
];
}
emitContext(lines.join(" "), "SessionStart");
Loading
Loading