diff --git a/bundle/.cursor/rules/00-gitnexus-enforcement.mdc b/bundle/.cursor/rules/00-gitnexus-enforcement.mdc index d2661a4..683722b 100644 --- a/bundle/.cursor/rules/00-gitnexus-enforcement.mdc +++ b/bundle/.cursor/rules/00-gitnexus-enforcement.mdc @@ -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 -- ""` 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 -- ""`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `` 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. diff --git a/bundle/.gnkit/lib/session-primer.mjs b/bundle/.gnkit/lib/session-primer.mjs index 5fb990d..b3bedbd 100644 --- a/bundle/.gnkit/lib/session-primer.mjs +++ b/bundle/.gnkit/lib/session-primer.mjs @@ -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 { diff --git a/bundle/scripts/gitnexus-agent.mjs b/bundle/scripts/gitnexus-agent.mjs index 25fee26..77ffe26 100644 --- a/bundle/scripts/gitnexus-agent.mjs +++ b/bundle/scripts/gitnexus-agent.mjs @@ -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 ""|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 ""|fallback:off|fallback-log [--json] */ import { spawnSync } from "node:child_process"; import fs from "node:fs"; @@ -22,6 +22,8 @@ const { grantClassicalFallback, revokeClassicalFallback, fallbackGrant, + appendFallbackReport, + readFallbackReports, bumpScore, } = await import( pathToFileURL(path.join(ROOT, ".gnkit/lib/session-primer.mjs")).href @@ -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."); @@ -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 "" | fallback:off | fallback-log`, ); process.exit(2); diff --git a/bundle/scripts/gitnexus-teaching/script-gates.mjs b/bundle/scripts/gitnexus-teaching/script-gates.mjs index c37abf0..7f20fd7 100644 --- a/bundle/scripts/gitnexus-teaching/script-gates.mjs +++ b/bundle/scripts/gitnexus-teaching/script-gates.mjs @@ -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", }, }, { diff --git a/bundle/templates/AGENTS.gitnexus.md b/bundle/templates/AGENTS.gitnexus.md index a873318..3686b41 100644 --- a/bundle/templates/AGENTS.gitnexus.md +++ b/bundle/templates/AGENTS.gitnexus.md @@ -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 -- ""` 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 -- ""`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `` 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. diff --git a/bundle/templates/CLAUDE.gitnexus.md b/bundle/templates/CLAUDE.gitnexus.md index 77f85a1..003032b 100644 --- a/bundle/templates/CLAUDE.gitnexus.md +++ b/bundle/templates/CLAUDE.gitnexus.md @@ -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 -- ""` 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 -- ""`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `` 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. diff --git a/lib/kit.test.mjs b/lib/kit.test.mjs index 4fc5bcf..8227c3f 100644 --- a/lib/kit.test.mjs +++ b/lib/kit.test.mjs @@ -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( diff --git a/scripts/contract/enforcement-contract.md b/scripts/contract/enforcement-contract.md index 912859e..6c9e652 100644 --- a/scripts/contract/enforcement-contract.md +++ b/scripts/contract/enforcement-contract.md @@ -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 -- ""` 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 -- ""`, which opens ~15 min where classical Grep/Read/shell are allowed (auto-resumes; end early with `npm run gitnexus:fallback:off`). **Make `` 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.