Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion bundle/.cursor/rules/00-gitnexus-enforcement.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,6 @@ On recovery **READ the task-core FIRST** and reconstruct from it — it's the on

**Stale index** → run `agent-refresh` first; classical Grep/Read stay denied until it succeeds. **If refresh fails** (or MCP down): classical Grep/Read OK — one-sentence why.

**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"` opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). It is logged to telemetry and shown in `gitnexus:status` + the session brief — re-confirm findings with the graph once GN is reliable. Repeated grants signal a genuine GitNexus problem worth reporting.
**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `<why>` specific and actionable** — which GN tool, expected vs actual (e.g. `impact returned 0 callers for OrderService but grep finds 3`) — because it's appended as a **GitNexus failure report** (`npm run gitnexus:fallback-log`), captured with the graph state, for the GitNexus developers. Re-confirm findings with the graph once GN is reliable; repeated reports pinpoint where GN needs fixing.

Optional: `GITNEXUS_MODE=guide` (nudge-only). Paths: `.gnkit/gitnexus-hooks.json`. Playbooks: `gitnexus-enforcement` skill.
71 changes: 71 additions & 0 deletions bundle/.gnkit/lib/session-primer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,77 @@ export function grantClassicalFallback(root, reason = '', ttlMs = FALLBACK_TTL_M
);
}

// ── Fallback → telemetry bridge (a "where did GitNexus fail" report log) ──────
// The grant flag is transient (expires/clears). To see WHERE GitNexus fell short and
// report it upstream, every fallback also appends a durable record — the reason plus the
// graph state it distrusted (version, size, index commit/age) — to an append-only log.
// Read via `gitnexus:fallback-log` (+ `--json` to export for the GitNexus developers).

const FALLBACK_LOG_FILE = '.gitnexus-fallback-log.jsonl';

/** @param {string} root — append-only fallback-report log (gitignored, never cleared). */
export function fallbackLogPath(root) {
return path.join(root, '.gnkit', FALLBACK_LOG_FILE);
}

/**
* Append a fallback report: the agent's stated reason + the graph state it distrusted, so
* the user can review where GitNexus fell short and report it to the GN developers.
* @param {string} root @param {string} reason @returns {boolean} written?
*/
export function appendFallbackReport(root, reason) {
let meta = {};
try {
meta = JSON.parse(fs.readFileSync(path.join(root, '.gitnexus/meta.json'), 'utf8'));
} catch {
/* index may be missing — that itself is context */
}
const s = meta.stats || {};
const rec = {
at: new Date().toISOString(),
repo: repoName(root),
reason: String(reason || '').slice(0, 1000),
gitnexusVersion: meta.version ?? meta.gitnexusVersion ?? null,
index: {
files: s.files ?? null,
nodes: s.nodes ?? null,
edges: s.edges ?? null,
embeddings: s.embeddings ?? null,
processes: s.processes ?? null,
},
indexedCommit: meta.lastCommit ?? null,
indexedAt: meta.indexedAt ?? null,
};
try {
fs.mkdirSync(path.join(root, '.gnkit'), { recursive: true });
fs.appendFileSync(fallbackLogPath(root), JSON.stringify(rec) + '\n');
return true;
} catch {
return false;
}
}

/** Parse the fallback-report log into records (skips blank/malformed lines). */
export function readFallbackReports(root) {
let text = '';
try {
text = fs.readFileSync(fallbackLogPath(root), 'utf8');
} catch {
return [];
}
const out = [];
for (const line of text.split('\n')) {
const t = line.trim();
if (!t) continue;
try {
out.push(JSON.parse(t));
} catch {
/* skip malformed line */
}
}
return out;
}

/** @param {string} root */
export function revokeClassicalFallback(root) {
try {
Expand Down
39 changes: 37 additions & 2 deletions bundle/scripts/gitnexus-agent.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Agent-facing GitNexus maintenance CLI (no MCP required).
* Usage: node scripts/gitnexus-agent.mjs status|refresh|brief|health|verify|doctor|review [base]|pr-impact [base]|branch-status [base]|commit-msg|map|scorecard|stats [--json]|graph-smoke|detect-api|fallback "<why>"|fallback:off
* Usage: node scripts/gitnexus-agent.mjs status|refresh|brief|health|verify|doctor|review [base]|pr-impact [base]|branch-status [base]|commit-msg|map|scorecard|stats [--json]|graph-smoke|detect-api|fallback "<why>"|fallback:off|fallback-log [--json]
*/
import { spawnSync } from "node:child_process";
import fs from "node:fs";
Expand All @@ -22,6 +22,8 @@ const {
grantClassicalFallback,
revokeClassicalFallback,
fallbackGrant,
appendFallbackReport,
readFallbackReports,
bumpScore,
} = await import(
pathToFileURL(path.join(ROOT, ".gnkit/lib/session-primer.mjs")).href
Expand Down Expand Up @@ -70,16 +72,43 @@ if (cmd === "fallback") {
}
grantClassicalFallback(ROOT, reason);
bumpScore(ROOT, "classicalFallbackGranted");
appendFallbackReport(ROOT, reason); // durable "where GitNexus fell short" report
const g = fallbackGrant(ROOT);
const mins = g ? Math.max(1, Math.round(g.remainingMs / 60000)) : 15;
console.log(`⚠ Classical fallback GRANTED for ~${mins} min — reason: ${reason}`);
console.log(
" Classical Grep/Read/shell are now allowed. Re-confirm findings with the graph once GitNexus is reliable.",
);
console.log(" Logged for review → npm run gitnexus:fallback-log (report these to the GitNexus devs).");
console.log(" End early: npm run gitnexus:fallback:off");
process.exit(0);
}

if (cmd === "fallback-log") {
const reports = readFallbackReports(ROOT);
if (process.argv.includes("--json")) {
console.log(JSON.stringify(reports, null, 2));
process.exit(0);
}
if (!reports.length) {
console.log("No GitNexus fallback reports yet — agents haven't distrusted the graph in this repo.");
process.exit(0);
}
console.log(
`GitNexus fallback reports — ${reports.length} time(s) an agent distrusted the graph (report these upstream):\n`,
);
for (const r of reports.slice(-30)) {
const idx = r.index || {};
const size = idx.nodes != null ? `${idx.nodes} nodes/${idx.embeddings ?? "?"} emb` : "no index";
const ver = r.gitnexusVersion ? ` v${r.gitnexusVersion}` : "";
console.log(`• ${r.at} · ${r.repo}${ver} · ${size} · indexed ${(r.indexedCommit || "?").slice(0, 7)}`);
console.log(` ${r.reason}`);
}
if (reports.length > 30) console.log(`\n(showing last 30 of ${reports.length}; --json for all)`);
console.log("\nExport for the GitNexus developers: npm run gitnexus:fallback-log -- --json");
process.exit(0);
}

if (cmd === "fallback:off" || cmd === "unfallback") {
revokeClassicalFallback(ROOT);
console.log("Classical fallback ended — graph-first enforcement re-armed.");
Expand Down Expand Up @@ -601,10 +630,16 @@ if (cmd === "stats") {
`\n Value: ${redir} lazy-search redirect(s) to the graph, ${gate} pre-edit/commit gate(s) fired.`,
);
console.log(` Log: ${path.join(".gnkit", ".gitnexus-telemetry.jsonl")}`);
const fb = readFallbackReports(ROOT);
if (fb.length) {
console.log(
`\n ⚠ GitNexus fallback reports: ${fb.length} — where the graph fell short. See: npm run gitnexus:fallback-log`,
);
}
process.exit(0);
}

console.error(
`Unknown command: ${cmd}. Use: status | refresh | brief | health | verify | doctor | review [base] | pr-impact [base] | branch-status [base] | commit-msg | map | scorecard | stats | graph-smoke | detect-api`,
`Unknown command: ${cmd}. Use: status | refresh | brief | health | verify | doctor | review [base] | pr-impact [base] | branch-status [base] | commit-msg | map | scorecard | stats | graph-smoke | detect-api | fallback "<why>" | fallback:off | fallback-log`,
);
process.exit(2);
1 change: 1 addition & 0 deletions bundle/scripts/gitnexus-teaching/script-gates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const GITNEXUS_SCRIPT_GATES = [
"gitnexus:map": "node scripts/gitnexus-agent.mjs map",
"gitnexus:fallback": "node scripts/gitnexus-agent.mjs fallback",
"gitnexus:fallback:off": "node scripts/gitnexus-agent.mjs fallback:off",
"gitnexus:fallback-log": "node scripts/gitnexus-agent.mjs fallback-log",
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion bundle/templates/AGENTS.gitnexus.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ On recovery **READ the task-core FIRST** and reconstruct from it — it's the on

**Stale index** → run `agent-refresh` first; classical Grep/Read stay denied until it succeeds. **If refresh fails** (or MCP down): classical Grep/Read OK — one-sentence why.

**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"` opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). It is logged to telemetry and shown in `gitnexus:status` + the session brief — re-confirm findings with the graph once GN is reliable. Repeated grants signal a genuine GitNexus problem worth reporting.
**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `<why>` specific and actionable** — which GN tool, expected vs actual (e.g. `impact returned 0 callers for OrderService but grep finds 3`) — because it's appended as a **GitNexus failure report** (`npm run gitnexus:fallback-log`), captured with the graph state, for the GitNexus developers. Re-confirm findings with the graph once GN is reliable; repeated reports pinpoint where GN needs fixing.

Optional: `GITNEXUS_MODE=guide` (nudge-only). Paths: `.gnkit/gitnexus-hooks.json`. Playbooks: `gitnexus-enforcement` skill.

Expand Down
2 changes: 1 addition & 1 deletion bundle/templates/CLAUDE.gitnexus.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ On recovery **READ the task-core FIRST** and reconstruct from it — it's the on

**Stale index** → run `agent-refresh` first; classical Grep/Read stay denied until it succeeds. **If refresh fails** (or MCP down): classical Grep/Read OK — one-sentence why.

**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"` opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). It is logged to telemetry and shown in `gitnexus:status` + the session brief — re-confirm findings with the graph once GN is reliable. Repeated grants signal a genuine GitNexus problem worth reporting.
**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `<why>` specific and actionable** — which GN tool, expected vs actual (e.g. `impact returned 0 callers for OrderService but grep finds 3`) — because it's appended as a **GitNexus failure report** (`npm run gitnexus:fallback-log`), captured with the graph state, for the GitNexus developers. Re-confirm findings with the graph once GN is reliable; repeated reports pinpoint where GN needs fixing.

Optional: `GITNEXUS_MODE=guide` (nudge-only). Paths: `.gnkit/gitnexus-hooks.json`. Playbooks: `gitnexus-enforcement` skill.

Expand Down
44 changes: 44 additions & 0 deletions lib/kit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,50 @@ describe("gitnexus-agent-kit", () => {
fs.rmSync(tmp, { recursive: true, force: true });
});

it("fallback reports persist the reason + graph state for upstream review", async () => {
const sp = await import(
new URL("../bundle/.gnkit/lib/session-primer.mjs", import.meta.url).href
);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gn-fbrep-"));
fs.mkdirSync(path.join(tmp, ".gitnexus"), { recursive: true });
fs.writeFileSync(
path.join(tmp, ".gitnexus/meta.json"),
JSON.stringify({
version: "1.6.9",
lastCommit: "abcdef1234567",
indexedAt: "2026-07-17T00:00:00Z",
stats: { files: 100, nodes: 42000, edges: 91000, embeddings: 5117, processes: 300 },
}),
);

assert.equal(sp.readFallbackReports(tmp).length, 0);
assert.equal(
sp.appendFallbackReport(tmp, "impact returned 0 callers for OrderService but grep finds 3"),
true,
);
sp.appendFallbackReport(tmp, "second reason");
const r = sp.readFallbackReports(tmp);
assert.equal(r.length, 2, "append-only log");
assert.match(r[0].reason, /OrderService/);
assert.equal(r[0].index.nodes, 42000);
assert.equal(r[0].index.embeddings, 5117);
assert.equal(r[0].gitnexusVersion, "1.6.9");
assert.equal(r[0].indexedCommit, "abcdef1234567");
assert.equal(r[0].repo, path.basename(tmp));

// Durable: survives a session clear (it's a report log, not session state).
sp.clearSessionState(tmp);
assert.equal(sp.readFallbackReports(tmp).length, 2, "reports survive session clear");

// Missing index → still logs the reason (a missing index is itself a signal).
fs.rmSync(path.join(tmp, ".gitnexus"), { recursive: true, force: true });
sp.appendFallbackReport(tmp, "GN index missing");
const r2 = sp.readFallbackReports(tmp);
assert.equal(r2.length, 3);
assert.equal(r2[2].index.nodes, null);
fs.rmSync(tmp, { recursive: true, force: true });
});

it("session-health-audit builds agent context and user message", async () => {
const auditMod = await import(
new URL(
Expand Down
2 changes: 1 addition & 1 deletion scripts/contract/enforcement-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,6 @@ On recovery **READ the task-core FIRST** and reconstruct from it — it's the on

**Stale index** → run `agent-refresh` first; classical Grep/Read stay denied until it succeeds. **If refresh fails** (or MCP down): classical Grep/Read OK — one-sentence why.

**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"` opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). It is logged to telemetry and shown in `gitnexus:status` + the session brief — re-confirm findings with the graph once GN is reliable. Repeated grants signal a genuine GitNexus problem worth reporting.
**GitNexus fresh but wrong / suspicious / incomplete?** Don't silently fight the gate — take the escape hatch: `npm run gitnexus:fallback -- "<why>"`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `<why>` specific and actionable** — which GN tool, expected vs actual (e.g. `impact returned 0 callers for OrderService but grep finds 3`) — because it's appended as a **GitNexus failure report** (`npm run gitnexus:fallback-log`), captured with the graph state, for the GitNexus developers. Re-confirm findings with the graph once GN is reliable; repeated reports pinpoint where GN needs fixing.

Optional: `GITNEXUS_MODE=guide` (nudge-only). Paths: `.gnkit/gitnexus-hooks.json`. Playbooks: `gitnexus-enforcement` skill.
Loading