mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
feat: context window monitor hook with agent-side WARNING/CRITICAL alerts
Adds PostToolUse hook that reads context metrics from statusline bridge file and injects alerts into agent conversation when context is low. Features: - Two-tier alerts: WARNING (<=35% remaining) and CRITICAL (<=25%) - Smart debounce: 5 tool uses between warnings, severity escalation bypasses - Silent fail: never blocks tool execution - Security: session_id sanitized to prevent path traversal Ref #212
This commit is contained in:
@@ -892,7 +892,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
// 4. Remove GSD hooks
|
||||
const hooksDir = path.join(targetDir, 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh'];
|
||||
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-context-monitor.js'];
|
||||
let hookCount = 0;
|
||||
for (const hook of gsdHooks) {
|
||||
const hookPath = path.join(hooksDir, hook);
|
||||
@@ -958,10 +958,32 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
if (settings.hooks.SessionStart.length === 0) {
|
||||
delete settings.hooks.SessionStart;
|
||||
}
|
||||
// Clean up empty hooks object
|
||||
if (Object.keys(settings.hooks).length === 0) {
|
||||
delete settings.hooks;
|
||||
}
|
||||
|
||||
// Remove GSD hooks from PostToolUse
|
||||
if (settings.hooks && settings.hooks.PostToolUse) {
|
||||
const before = settings.hooks.PostToolUse.length;
|
||||
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
|
||||
if (entry.hooks && Array.isArray(entry.hooks)) {
|
||||
const hasGsdHook = entry.hooks.some(h =>
|
||||
h.command && h.command.includes('gsd-context-monitor')
|
||||
);
|
||||
return !hasGsdHook;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (settings.hooks.PostToolUse.length < before) {
|
||||
settingsModified = true;
|
||||
console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
|
||||
}
|
||||
if (settings.hooks.PostToolUse.length === 0) {
|
||||
delete settings.hooks.PostToolUse;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up empty hooks object
|
||||
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
||||
delete settings.hooks;
|
||||
}
|
||||
|
||||
if (settingsModified) {
|
||||
@@ -1520,6 +1542,9 @@ function install(isGlobal, runtime = 'claude') {
|
||||
const updateCheckCommand = isGlobal
|
||||
? buildHookCommand(targetDir, 'gsd-check-update.js')
|
||||
: 'node ' + dirName + '/hooks/gsd-check-update.js';
|
||||
const contextMonitorCommand = isGlobal
|
||||
? buildHookCommand(targetDir, 'gsd-context-monitor.js')
|
||||
: 'node ' + dirName + '/hooks/gsd-context-monitor.js';
|
||||
|
||||
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
|
||||
if (isGemini) {
|
||||
@@ -1556,6 +1581,27 @@ function install(isGlobal, runtime = 'claude') {
|
||||
});
|
||||
console.log(` ${green}✓${reset} Configured update check hook`);
|
||||
}
|
||||
|
||||
// Configure PostToolUse hook for context window monitoring
|
||||
if (!settings.hooks.PostToolUse) {
|
||||
settings.hooks.PostToolUse = [];
|
||||
}
|
||||
|
||||
const hasContextMonitorHook = settings.hooks.PostToolUse.some(entry =>
|
||||
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))
|
||||
);
|
||||
|
||||
if (!hasContextMonitorHook) {
|
||||
settings.hooks.PostToolUse.push({
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: contextMonitorCommand
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log(` ${green}✓${reset} Configured context window monitor hook`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write file manifest for future modification detection
|
||||
|
||||
96
docs/context-monitor.md
Normal file
96
docs/context-monitor.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Context Window Monitor
|
||||
|
||||
A PostToolUse hook that warns the agent when context window usage is high.
|
||||
|
||||
## Problem
|
||||
|
||||
The statusline shows context usage to the **user**, but the **agent** has no awareness of context limits. When context runs low, the agent continues working until it hits the wall — potentially mid-task with no state saved.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The statusline hook writes context metrics to `/tmp/claude-ctx-{session_id}.json`
|
||||
2. After each tool use, the context monitor reads these metrics
|
||||
3. When remaining context drops below thresholds, it injects a warning as `additionalContext`
|
||||
4. The agent receives the warning in its conversation and can act accordingly
|
||||
|
||||
## Thresholds
|
||||
|
||||
| Level | Remaining | Agent Behavior |
|
||||
|-------|-----------|----------------|
|
||||
| Normal | > 35% | No warning |
|
||||
| WARNING | <= 35% | Wrap up current task, avoid starting new complex work |
|
||||
| CRITICAL | <= 25% | Stop immediately, save state (`/gsd:pause-work`) |
|
||||
|
||||
## Debounce
|
||||
|
||||
To avoid spamming the agent with repeated warnings:
|
||||
- First warning always fires immediately
|
||||
- Subsequent warnings require 5 tool uses between them
|
||||
- Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Statusline Hook (gsd-statusline.js)
|
||||
| writes
|
||||
v
|
||||
/tmp/claude-ctx-{session_id}.json
|
||||
^ reads
|
||||
|
|
||||
Context Monitor (gsd-context-monitor.js, PostToolUse)
|
||||
| injects
|
||||
v
|
||||
additionalContext -> Agent sees warning
|
||||
```
|
||||
|
||||
The bridge file is a simple JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"remaining_percentage": 28.5,
|
||||
"used_pct": 71,
|
||||
"timestamp": 1708200000
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with GSD
|
||||
|
||||
GSD's `/gsd:pause-work` command saves execution state. The WARNING message suggests using it. The CRITICAL message instructs immediate state save.
|
||||
|
||||
## Setup
|
||||
|
||||
Both hooks are automatically registered during `npx get-shit-done-cc` installation:
|
||||
|
||||
- **Statusline** (writes bridge file): Registered as `statusLine` in settings.json
|
||||
- **Context Monitor** (reads bridge file): Registered as `PostToolUse` hook in settings.json
|
||||
|
||||
Manual registration in `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "node ~/.claude/hooks/gsd-statusline.js"
|
||||
},
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ~/.claude/hooks/gsd-context-monitor.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
- The hook wraps everything in try/catch and exits silently on error
|
||||
- It never blocks tool execution — a broken monitor should not break the agent's workflow
|
||||
- Stale metrics (older than 60s) are ignored
|
||||
- Missing bridge files are handled gracefully (subagents, fresh sessions)
|
||||
122
hooks/gsd-context-monitor.js
Normal file
122
hooks/gsd-context-monitor.js
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
// Context Monitor - PostToolUse hook
|
||||
// Reads context metrics from the statusline bridge file and injects
|
||||
// warnings when context usage is high. This makes the AGENT aware of
|
||||
// context limits (the statusline only shows the user).
|
||||
//
|
||||
// How it works:
|
||||
// 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
|
||||
// 2. This hook reads those metrics after each tool use
|
||||
// 3. When remaining context drops below thresholds, it injects a warning
|
||||
// as additionalContext, which the agent sees in its conversation
|
||||
//
|
||||
// Thresholds:
|
||||
// WARNING (remaining <= 35%): Agent should wrap up current task
|
||||
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
||||
//
|
||||
// Debounce: 5 tool uses between warnings to avoid spam
|
||||
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
||||
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
||||
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
||||
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
const sessionId = data.session_id;
|
||||
|
||||
if (!sessionId) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tmpDir = os.tmpdir();
|
||||
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
||||
|
||||
// If no metrics file, this is a subagent or fresh session -- exit silently
|
||||
if (!fs.existsSync(metricsPath)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Ignore stale metrics
|
||||
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const remaining = metrics.remaining_percentage;
|
||||
const usedPct = metrics.used_pct;
|
||||
|
||||
// No warning needed
|
||||
if (remaining > WARNING_THRESHOLD) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Debounce: check if we warned recently
|
||||
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
||||
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
||||
let firstWarn = true;
|
||||
|
||||
if (fs.existsSync(warnPath)) {
|
||||
try {
|
||||
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
||||
firstWarn = false;
|
||||
} catch (e) {
|
||||
// Corrupted file, reset
|
||||
}
|
||||
}
|
||||
|
||||
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
||||
|
||||
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
||||
const currentLevel = isCritical ? 'critical' : 'warning';
|
||||
|
||||
// Emit immediately on first warning, then debounce subsequent ones
|
||||
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
||||
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
||||
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
||||
// Update counter and exit without warning
|
||||
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Reset debounce counter
|
||||
warnData.callsSinceWarn = 0;
|
||||
warnData.lastLevel = currentLevel;
|
||||
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
||||
|
||||
// Build warning message
|
||||
let message;
|
||||
if (isCritical) {
|
||||
message = `CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
|
||||
'If using GSD, run /gsd:pause-work to save execution state.';
|
||||
} else {
|
||||
message = `CONTEXT MONITOR WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
||||
'Begin wrapping up current task. Do not start new complex work. ' +
|
||||
'If using GSD, consider /gsd:pause-work to save state.';
|
||||
}
|
||||
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PostToolUse",
|
||||
additionalContext: message
|
||||
}
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
} catch (e) {
|
||||
// Silent fail -- never block tool execution
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -27,6 +27,23 @@ process.stdin.on('end', () => {
|
||||
// Scale: 80% real usage = 100% displayed
|
||||
const used = Math.min(100, Math.round((rawUsed / 80) * 100));
|
||||
|
||||
// Write context metrics to bridge file for the context-monitor PostToolUse hook.
|
||||
// The monitor reads this file to inject agent-facing warnings when context is low.
|
||||
if (session) {
|
||||
try {
|
||||
const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
|
||||
const bridgeData = JSON.stringify({
|
||||
session_id: session,
|
||||
remaining_percentage: remaining,
|
||||
used_pct: used,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
fs.writeFileSync(bridgePath, bridgeData);
|
||||
} catch (e) {
|
||||
// Silent fail -- bridge is best-effort, don't break statusline
|
||||
}
|
||||
}
|
||||
|
||||
// Build progress bar (10 segments)
|
||||
const filled = Math.floor(used / 10);
|
||||
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
||||
|
||||
@@ -12,6 +12,7 @@ const DIST_DIR = path.join(HOOKS_DIR, 'dist');
|
||||
// Hooks to copy (pure Node.js, no bundling needed)
|
||||
const HOOKS_TO_COPY = [
|
||||
'gsd-check-update.js',
|
||||
'gsd-context-monitor.js',
|
||||
'gsd-statusline.js'
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user