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
57 changes: 57 additions & 0 deletions bundle/.claude/hooks/gitnexus-context-pressure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env node
// Claude Code PostToolUse → estimate how full the context window is; when auto-compaction is
// NEAR, nudge the agent to refresh its TASK-CORE (dense AI save-state) BEFORE the summary drops
// detail. PreCompact can't inject context or make the agent act, so this PostToolUse hook is
// where the pre-compaction "migrate the task" trigger lives (additionalContext is valid here).
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 transcript = input.transcript_path || "";
if (!transcript) process.exit(0);

const lib = (rel) => import(pathToFileURL(path.join(root, ".gnkit/lib", rel)).href);
const { loadHookConfig } = await lib("hook-helpers.mjs");
const { contextPressure } = await lib("context-pressure.mjs");
const { emitContext } = await lib("claude-emit.mjs");
const { taskCoreExists, isPressureNudged, setPressureNudged, bumpScore } =
await lib("session-primer.mjs");

const config = loadHookConfig(root);
if (!(config.contextPressureThreshold > 0)) process.exit(0); // feature disabled

const p = contextPressure(transcript, config);

if (p.over) {
// Nudge once per pressure zone (flag) — but keep nudging while there's still NO task-core,
// since compacting with no core is straight data loss. Once a core exists, go quiet.
if (!isPressureNudged(root) || !taskCoreExists(root)) {
const pct = Math.round(p.ratio * 100);
const kt = Math.round(p.tokens / 1000);
emitContext(
`⚠ CONTEXT ~${pct}% full (~${kt}k tok) — auto-compaction is NEAR. Refresh your TASK-CORE ` +
"**now**, before the summary drops load-bearing detail: write `.gnkit/.gitnexus-task-core.md` " +
"as a DENSE, AI-facing save-state of THIS TASK — GOAL · CONSTRAINTS · DECISIONS(+why) · " +
"STATE(done/now/NEXT/todo) · ANCHORS(file:line) · GOTCHAS(failed approaches, traps) · " +
"OPEN-Qs · this-task USER-PREFS. Terse, no prose — it's for you, not humans. It is the ONE " +
"thing guaranteed to survive compaction; SessionStart reads it back on recovery. " +
"(Format: the `gitnexus-taskcore` skill.)",
"PostToolUse",
);
setPressureNudged(root, true);
bumpScore(root, "contextPressureNudges");
}
} else if (p.ratio < config.contextPressureThreshold - 0.1) {
// Pressure fell well below the line (a compaction shrank the transcript) → re-arm the nudge.
setPressureNudged(root, false);
}
// otherwise: stay silent (hysteresis band, or over-threshold with a fresh core already written)
7 changes: 7 additions & 0 deletions bundle/.claude/hooks/gitnexus-session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const {
isDetectUsed,
memoryPath,
fallbackGrant,
taskCorePath,
taskCoreExists,
} = await lib("session-primer.mjs");

const source = input.source || "startup";
Expand All @@ -43,8 +45,13 @@ const staleLine = grant
let lines;
if (recovering) {
const hasMem = existsSync(memoryPath(root));
const hasCore = taskCoreExists(root);
const tcp = taskCorePath(root);
lines = [
`GitNexus: context was ${source === "compact" ? "COMPACTED" : "resumed"} — the task CONTINUES; enforcement and this session's satisfied gates are PRESERVED.`,
hasCore
? `READ your TASK-CORE FIRST — \`${tcp}\`: a dense save-state of THIS task (goal/constraints/decisions/state/anchors/gotchas/next). Reconstruct from it, verify against reality, then continue — do not re-derive what it already settles.`
: `No TASK-CORE saved — reconstruct THIS task (goal/decisions/state/next) from your memory + the code before acting, and write \`.gnkit/.gitnexus-task-core.md\` next time so compaction can't drift you.`,
// 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.",
Expand Down
12 changes: 12 additions & 0 deletions bundle/.cursor/rules/00-gitnexus-enforcement.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ At a **milestone** — feature done / big-task checkpoint / shared-code refactor

Maintain your **Claude Code project memory** — `~/.claude/projects/<this-project>/memory/MEMORY.md` (Claude Code's native memory; **all agents share this one file** — Claude refers to its own, other agents mirror it). Record task, key decisions, findings, open items, important `file:line`. Update it at milestones and whenever you conclude something that must outlive the current transcript. Context compaction and new sessions drop the conversation; this file does not. On recovery (post-compaction/resume) READ it first and reconcile it with reality — **nothing important may be lost.**

## Task-core (survive compaction without drift)

Long tasks get **compacted** — the transcript is summarized and dropped, and detail drifts. Keep a **task-core**: a dense, **AI-facing** save-state of the CURRENT TASK at **`.gnkit/.gitnexus-task-core.md`**. When a PostToolUse nudge says context is filling (~90%), or at a milestone / before a risky pivot, **write or refresh it** — terse, for *you* not humans, no prose tax:

```
GOAL <what "done" is> · CONSTRAINTS <must/never> · DECISIONS <choice→why (settled)>
STATE done<✓+anchor> / now / NEXT<exact> / todo<ordered> · ANCHORS <file:line→what/why>
GOTCHAS <failed approaches, traps, non-obvious> · OPEN-Qs · USER-PREFS(this task)
```

On recovery **READ the task-core FIRST** and reconstruct from it — it's the one thing guaranteed to survive with full detail. It's distinct from `MEMORY.md` (durable, cross-session, human-shared): the task-core is the *hot working-set for THIS task*, overwritten when the task changes. Full routine: the **`gitnexus-taskcore`** skill.

## Fallback

**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.
Expand Down
85 changes: 85 additions & 0 deletions bundle/.gnkit/lib/context-pressure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Context-pressure estimation for the TASK-CORE compaction-migration routine.
*
* Claude Code auto-compacts (summarizes + drops the transcript) when the context window
* fills. The PreCompact hook CANNOT make the agent act or inject context, so we can't wait
* for it. Instead a PostToolUse hook estimates how full the window is and, past a threshold,
* nudges the agent to write/refresh its TASK-CORE *before* the summary lands — the only thing
* guaranteed to survive compaction with full detail.
*
* This module is the estimator: it reads the CURRENT context size from the transcript cheaply
* (tail-read, no full-file parse) and accurately (the last assistant message's usage = the
* exact prompt size the model saw), with a byte-size fallback.
*/
import fs from "node:fs";

const TAIL_BYTES = 131072; // 128 KB tail — enough to hold the last usage record

/**
* Estimate the current context size in tokens from a Claude Code transcript (JSONL).
* @param {string} transcriptPath
* @returns {number} estimated context tokens (0 if unknown/unreadable)
*/
export function estimateContextTokens(transcriptPath) {
let size = 0;
try {
size = fs.statSync(transcriptPath).size;
} catch {
return 0;
}
if (!size) return 0;

let text = "";
try {
const readBytes = Math.min(size, TAIL_BYTES);
const fd = fs.openSync(transcriptPath, "r");
try {
const buf = Buffer.alloc(readBytes);
fs.readSync(fd, buf, 0, readBytes, size - readBytes);
text = buf.toString("utf8");
} finally {
fs.closeSync(fd);
}
} catch {
return Math.round(size / 3.5); // couldn't read tail → byte estimate
}

// Accurate: the LAST assistant message's usage is the prompt size in the window
// (non-cached input + cache read + cache creation = everything sent to the model).
const lines = text.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (!line || line[0] !== "{") continue;
let obj;
try {
obj = JSON.parse(line);
} catch {
continue; // partial line (tail cut mid-record) or non-JSON — skip
}
const u = obj?.message?.usage || obj?.usage;
if (u && typeof u.input_tokens === "number") {
return (
(u.input_tokens || 0) +
(u.cache_read_input_tokens || 0) +
(u.cache_creation_input_tokens || 0)
);
}
}
// No usage record in the tail → rough byte estimate (JSONL overhead ~3.5 bytes/token).
return Math.round(size / 3.5);
}

/**
* @param {string} transcriptPath
* @param {{ contextWindowTokens?: number, contextPressureThreshold?: number }} config
* @returns {{ tokens: number, window: number, threshold: number, ratio: number, over: boolean }}
*/
export function contextPressure(transcriptPath, config = {}) {
const window = Number(config.contextWindowTokens) > 0 ? Number(config.contextWindowTokens) : 200000;
const threshold =
Number(config.contextPressureThreshold) > 0 ? Number(config.contextPressureThreshold) : 0.9;
const tokens = estimateContextTokens(transcriptPath);
const ratio = window > 0 ? tokens / window : 0;
return { tokens, window, threshold, ratio, over: ratio >= threshold };
}
9 changes: 9 additions & 0 deletions bundle/.gnkit/lib/hook-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export function loadHookConfig(root) {
// Working-tree drift: after this many uncommitted source edits since the index,
// 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.
contextWindowTokens: 200000,
contextPressureThreshold: 0.9,
};

const cfgPath = path.join(root, CONFIG_FILE);
Expand All @@ -106,6 +111,10 @@ export function loadHookConfig(root) {
cfg.stalenessCacheTtlMs = file.stalenessCacheTtlMs;
if (typeof file.driftRefreshThreshold === "number")
cfg.driftRefreshThreshold = file.driftRefreshThreshold;
if (typeof file.contextWindowTokens === "number")
cfg.contextWindowTokens = file.contextWindowTokens;
if (typeof file.contextPressureThreshold === "number")
cfg.contextPressureThreshold = file.contextPressureThreshold;
if (Array.isArray(file.sourceGlobs) && file.sourceGlobs.length) {
cfg.sourcePathRes = file.sourceGlobs.map((g) => globToRegExp(g));
}
Expand Down
53 changes: 53 additions & 0 deletions bundle/.gnkit/lib/session-primer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,60 @@ export function sessionPaths(root) {
stalenessCacheFile: path.join(stateDir, '.gitnexus-staleness-cache.json'),
scorecardFile: path.join(stateDir, '.gitnexus-scorecard.json'),
fallbackFlag: path.join(stateDir, '.gitnexus-fallback.json'),
pressureNudgedFlag: path.join(stateDir, '.gitnexus-pressure-nudged.flag'),
};
}

// ── TASK-CORE (compaction-migration save-state) ──────────────────────────────
// A dense, AI-facing save-state of the CURRENT TASK (goal/constraints/decisions/state/
// anchors/gotchas/next). The context-pressure hook nudges the agent to refresh it before
// auto-compaction; the SessionStart(compact) recovery brief reads it back — so the task
// survives the summary without drift. Lives under .gnkit/ (gitignored, survives compaction
// AND new sessions since a task can span both; the agent overwrites it when the task changes).

/** @param {string} root */
export function taskCorePath(root) {
return path.join(root, '.gnkit', '.gitnexus-task-core.md');
}

/** @param {string} root @returns {boolean} does a task-core exist + have content? */
export function taskCoreExists(root) {
try {
return fs.statSync(taskCorePath(root)).size > 0;
} catch {
return false;
}
}

/** Age of the task-core in ms (Infinity if none) — the pressure hook nudges harder when stale. */
export function taskCoreAgeMs(root) {
try {
return Date.now() - fs.statSync(taskCorePath(root)).mtimeMs;
} catch {
return Infinity;
}
}

/** @param {string} root @param {boolean} on — remember we already nudged this pressure zone. */
export function setPressureNudged(root, on) {
const { stateDir, pressureNudgedFlag } = sessionPaths(root);
try {
if (on) {
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(pressureNudgedFlag, new Date().toISOString());
} else {
fs.unlinkSync(pressureNudgedFlag);
}
} catch {
/* best effort */
}
}

/** @param {string} root */
export function isPressureNudged(root) {
return fs.existsSync(sessionPaths(root).pressureNudgedFlag);
}

// ── Classical fallback escape hatch ──────────────────────────────────────────
// When GitNexus returns wrong / suspicious / incomplete info while the index is
// FRESH, graph-first enforcement would otherwise trap the agent. grantClassicalFallback
Expand Down Expand Up @@ -362,6 +413,7 @@ export function clearSessionState(root) {
stalenessCacheFile,
scorecardFile,
fallbackFlag,
pressureNudgedFlag,
} = sessionPaths(root);
fs.mkdirSync(stateDir, { recursive: true });
// Archive the finishing session's tally BEFORE wiping the scorecard.
Expand All @@ -376,6 +428,7 @@ export function clearSessionState(root) {
stalenessCacheFile,
scorecardFile,
fallbackFlag,
pressureNudgedFlag,
]) {
try {
fs.unlinkSync(f);
Expand Down
1 change: 1 addition & 0 deletions bundle/scripts/gitnexus-agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ if (cmd === "scorecard") {
compactions: "Context compactions",
classicalFallbackGranted: "Classical-fallback grants (GN distrusted)",
driftRefreshBlocks: "Graph-drift refresh blocks (edited since index)",
contextPressureNudges: "Context-pressure task-core nudges (near compaction)",
};
console.log("GitNexus enforcement scorecard (this session)");
console.log(
Expand Down
1 change: 1 addition & 0 deletions bundle/scripts/pack-gitnexus-teaching.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ BUNDLE_PATHS=(
.gnkit/lib/cursor-emit.mjs
.gnkit/lib/claude-emit.mjs
.gnkit/lib/session-primer.mjs
.gnkit/lib/context-pressure.mjs
.gnkit/lib/first-nudge.mjs
.gnkit/lib/clear-session.mjs
.gnkit/lib/set-refresh-pending.mjs
Expand Down
1 change: 1 addition & 0 deletions bundle/scripts/sync-cursor-gitnexus-teaching.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ HOOK_LIBS=(
".gnkit/lib/cursor-emit.mjs"
".gnkit/lib/claude-emit.mjs"
".gnkit/lib/session-primer.mjs"
".gnkit/lib/context-pressure.mjs"
".gnkit/lib/first-nudge.mjs"
".gnkit/lib/clear-session.mjs"
".gnkit/lib/set-refresh-pending.mjs"
Expand Down
63 changes: 63 additions & 0 deletions bundle/skills/gitnexus-taskcore/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: gitnexus-taskcore
description: "Maintain a dense, AI-facing TASK-CORE save-state so a long task survives context COMPACTION without drift. Load it when: a PostToolUse nudge says context is ~90% full, at a milestone / before a risky pivot / when the task shifts, OR on recovery after a compaction (read it back first). The core is for the model, not humans — terse, anchors over prose. Examples: \"context is filling — save state\", \"checkpoint the task before compaction\", \"recover the task after compaction\"."
---

# Task-core — a compaction save-state that kills drift

When a long task runs, Claude Code **compacts**: it summarizes the conversation and drops the transcript. The generic summary keeps the gist but loses load-bearing detail — a constraint the user gave, a decision's *why*, the exact file:line you were mid-edit on, a dead-end you already ruled out. After compaction the agent **drifts**: re-litigates settled calls, repeats failed approaches, forgets requirements.

The fix: **you** decide what survives. Keep a **task-core** — a dense, machine-facing save-state of the CURRENT TASK — and read it back on recovery. It's the one artifact guaranteed to survive with full fidelity.

**File:** `.gnkit/.gitnexus-task-core.md` (gitignored; survives compaction *and* new sessions — a task can span both; overwrite it when the task changes).

## When to write / refresh it

- **Context-pressure nudge** — a PostToolUse hook estimates the window and, at ~90% (`contextPressureThreshold` × `contextWindowTokens`, tunable in `.gnkit/gitnexus-hooks.json`), tells you compaction is near. **Refresh the core immediately** — this is the last reliable window before the summary lands.
- **Milestones** — a sub-goal done, a decision settled, a pivot. Cheap insurance so a *sudden* auto-compact never catches you with a stale core.
- **Task start / task shift** — seed a fresh core when a new task begins (don't carry the old one).

You don't need to rewrite it every turn — that wastes tokens. Refresh on the nudge and at real checkpoints.

## The format (dense, for the model — not humans)

Terse. No prose transitions, no politeness, no restating the obvious. **Anchors over narrative.** Optimize signal-per-token — the only reader is you, post-compaction.

```
# TASK-CORE — <one-line task> (refreshed @ <marker>)
GOAL: <what "done" looks like, measurable>
CONSTRAINTS: <hard invariants — must / never; the user's non-negotiables>
DECISIONS: <choice → why> (settled — so you don't re-litigate them)
STATE:
DONE: <✓ fact + file:line anchor>
NOW: <current sub-step>
NEXT: <the exact next action(s)>
TODO: <remaining, ordered>
ANCHORS: <file:line → what's there / why it matters> (your map to resume fast)
GOTCHAS: <failed approaches, traps, non-obvious facts — so you don't repeat them>
OPEN-Qs: <unresolved / needs a decision>
USER-PREFS(this task): <corrections + constraints the user gave THIS task>
```

**Include** the things a summary drops: the *why* behind decisions, dead-ends already ruled out, exact anchors, the user's precise wording on constraints, the immediate next action. **Exclude** narrative recap, tool-by-tool history, and anything re-derivable from the code in seconds.

## On recovery (post-compaction / resume)

The SessionStart brief points you here. **Read the task-core FIRST**, before acting:
1. Reconstruct the task from it — goal, constraints, decisions, state, next.
2. **Verify against reality** (the core is a point-in-time snapshot; a file may have moved). Don't trust an anchor blindly — confirm it.
3. Continue from `NEXT`. Do **not** re-derive what the core already settles, and do **not** repeat anything in `GOTCHAS`.

## Task-core vs. MEMORY.md

- **MEMORY.md** — durable, cross-session, human-shared *project* memory (who/what/why of the project over time).
- **Task-core** — the *hot working-set for THIS task*, machine-optimized, ephemeral, overwritten per task.

They complement: on recovery, read the **task-core first** (it's the current task), then reconcile with MEMORY.md for durable context.

## Anti-patterns

- Writing it human-pretty (headings, prose, hedging) — wastes the token budget it exists to save.
- Only writing it once at the start — refresh at the nudge and milestones or it goes stale.
- Dumping the whole transcript — the point is *distillation*: decisions, state, anchors, gotchas, next. If it reads like a diary, it's wrong.
- Trusting it blindly on recovery — always verify anchors against the live code.
1 change: 1 addition & 0 deletions bundle/skills/gitnexus-workspace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Run `npm run gitnexus:detect-api` to refresh the profile after major server chan
| Work across layers (controller→service→repo→model) | `gitnexus-layered-systems` |
| Structured task (pre-commit, PR, cross-module) | `gitnexus-scenarios` |
| Milestone deep audit (feature done, pre-ship, big refactor) | **`gitnexus-microscope`** — multi-lens, opinionated, verified |
| Context filling / near compaction / recover after compaction | **`gitnexus-taskcore`** — dense AI save-state, survives compaction without drift |
| PR or branch review | `gitnexus-pr-review` |
| Security / taint / injection review | `gitnexus-security-review` |
| Research HTTP API change | See **HTTP API routing** above |
Expand Down
Loading
Loading