diff --git a/bundle/.gnkit/lib/hook-helpers.mjs b/bundle/.gnkit/lib/hook-helpers.mjs index 55cf18a..9230b12 100644 --- a/bundle/.gnkit/lib/hook-helpers.mjs +++ b/bundle/.gnkit/lib/hook-helpers.mjs @@ -93,16 +93,17 @@ export function loadHookConfig(root) { // graph query tools require a fast incremental refresh. 0 disables the drift gate. driftRefreshThreshold: 3, // TASK-CORE compaction migration: nudge the agent to refresh its task-core once context - // reaches contextPressureThreshold of contextWindowTokens. Set contextWindowTokens to your - // model's window (200k default; 1M-context sessions → 1000000). 0 threshold disables. + // reaches contextPressureThreshold of contextWindowTokens. The window is model-specific, so + // prefer the per-machine env var GITNEXUS_CONTEXT_WINDOW=1000000 (wins over this file) rather + // than committing 1000000 to a shared repo where teammates run a 200k model. 0 threshold disables. contextWindowTokens: 200000, contextPressureThreshold: 0.9, }; const cfgPath = path.join(root, CONFIG_FILE); - if (!fs.existsSync(cfgPath)) return cfg; try { + // Missing file → readFileSync throws → caught below → defaults kept, then env override runs. const file = JSON.parse(fs.readFileSync(cfgPath, "utf8")); if (file.mode) cfg.mode = file.mode === "guide" ? "guide" : "enforce"; if (typeof file.readLineThreshold === "number") @@ -125,6 +126,12 @@ export function loadHookConfig(root) { /* keep defaults */ } + // Per-machine env override wins over the (team-shared) config file. The context window is + // model-specific — a 1M-context session and a teammate's 200k model can't share one committed + // value — so set GITNEXUS_CONTEXT_WINDOW=1000000 in your shell rather than in gitnexus-hooks.json. + const envWindow = Number(process.env.GITNEXUS_CONTEXT_WINDOW); + if (Number.isFinite(envWindow) && envWindow > 0) cfg.contextWindowTokens = envWindow; + return cfg; } diff --git a/lib/kit.test.mjs b/lib/kit.test.mjs index 8227c3f..cd632f4 100644 --- a/lib/kit.test.mjs +++ b/lib/kit.test.mjs @@ -1033,6 +1033,47 @@ describe("gitnexus-agent-kit", () => { fs.rmSync(tmp, { recursive: true, force: true }); }); + it("GITNEXUS_CONTEXT_WINDOW env overrides the shared config file (per-machine window)", async () => { + const helpers = await import( + new URL("../bundle/.gnkit/lib/hook-helpers.mjs", import.meta.url).href + ); + const prev = process.env.GITNEXUS_CONTEXT_WINDOW; + try { + const empty = fs.mkdtempSync(path.join(os.tmpdir(), "gn-win-")); + + // No file + no env → default window. + delete process.env.GITNEXUS_CONTEXT_WINDOW; + assert.equal(helpers.loadHookConfig(empty).contextWindowTokens, 200000); + + // No file + env → env applies (guards against the no-config early-return path). + process.env.GITNEXUS_CONTEXT_WINDOW = "1000000"; + assert.equal(helpers.loadHookConfig(empty).contextWindowTokens, 1000000); + + // File says 200k, env says 1M → env wins (model-specific, not committed to a shared repo). + const repo = fs.mkdtempSync(path.join(os.tmpdir(), "gn-win2-")); + fs.mkdirSync(path.join(repo, ".gnkit"), { recursive: true }); + fs.writeFileSync( + path.join(repo, ".gnkit/gitnexus-hooks.json"), + JSON.stringify({ contextWindowTokens: 200000 }), + ); + assert.equal(helpers.loadHookConfig(repo).contextWindowTokens, 1000000); + + // No env → the file value stands. + delete process.env.GITNEXUS_CONTEXT_WINDOW; + assert.equal(helpers.loadHookConfig(repo).contextWindowTokens, 200000); + + // Garbage env is ignored (no NaN/0 window). + process.env.GITNEXUS_CONTEXT_WINDOW = "banana"; + assert.equal(helpers.loadHookConfig(repo).contextWindowTokens, 200000); + + fs.rmSync(empty, { recursive: true, force: true }); + fs.rmSync(repo, { recursive: true, force: true }); + } finally { + if (prev === undefined) delete process.env.GITNEXUS_CONTEXT_WINDOW; + else process.env.GITNEXUS_CONTEXT_WINDOW = prev; + } + }); + it("cypher field access matches Methods (untyped source node)", async () => { const cypher = await import( new URL("../bundle/.gnkit/lib/cypher-helpers.mjs", import.meta.url)