#!/usr/bin/env node /** * Hook Validator: Token Checkpoint Enforcement * * Runs BEFORE any tool execution to enforce token checkpoint reporting. * Prevents "framework fade" where pressure checks are skipped. * * This is architectural enforcement - AI cannot bypass this check. * * Checks: * - Current token estimate vs next checkpoint * - If checkpoint overdue: BLOCK and force pressure check * * Exit codes: * 0 = PASS (allow tool execution) * 1 = FAIL (block - checkpoint overdue) * * Copyright 2025 Tractatus Project * Licensed under Apache License 2.0 */ const fs = require('fs'); const path = require('path'); const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json'); const TOKEN_CHECKPOINTS_PATH = path.join(__dirname, '../../.claude/token-checkpoints.json'); /** * Color output */ const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function error(message) { log(` āœ— ${message}`, 'red'); } function success(message) { log(` āœ“ ${message}`, 'green'); } /** * Estimate current token usage */ function estimateTokens() { try { if (!fs.existsSync(SESSION_STATE_PATH)) { return 0; } const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8')); // If token_estimate exists in session state, use it if (sessionState.token_estimate && sessionState.token_estimate > 0) { return sessionState.token_estimate; } // Otherwise estimate from message count // Rough approximation: 1500 tokens per message (conservative) const messageCount = sessionState.message_count || 0; return messageCount * 1500; } catch (err) { // If we can't read session state, assume 0 (fail safe) return 0; } } /** * Load checkpoint configuration */ function loadCheckpoints() { try { if (!fs.existsSync(TOKEN_CHECKPOINTS_PATH)) { // No checkpoints file = no enforcement return null; } return JSON.parse(fs.readFileSync(TOKEN_CHECKPOINTS_PATH, 'utf8')); } catch (err) { return null; } } /** * Check if checkpoint is overdue */ function checkOverdue(currentTokens, checkpoints) { if (!checkpoints) { return { overdue: false }; } // Find next incomplete checkpoint const nextCheckpoint = checkpoints.checkpoints.find(c => !c.completed); if (!nextCheckpoint) { // All checkpoints completed return { overdue: false }; } // Check if we've passed the checkpoint threshold const overdueThreshold = nextCheckpoint.tokens; const isOverdue = currentTokens >= overdueThreshold; return { overdue: isOverdue, checkpoint: nextCheckpoint, currentTokens: currentTokens, threshold: overdueThreshold, percentage: nextCheckpoint.percentage }; } /** * Main validation */ async function main() { // Estimate current token usage const currentTokens = estimateTokens(); // Load checkpoints const checkpoints = loadCheckpoints(); if (!checkpoints) { // No checkpoints configured - pass process.exit(0); } // Check if checkpoint overdue const status = checkOverdue(currentTokens, checkpoints); if (status.overdue) { log(`\nāš ļø TOKEN CHECKPOINT OVERDUE`, 'yellow'); log(``, 'reset'); error(`Current tokens: ~${status.currentTokens.toLocaleString()}`); error(`Checkpoint: ${status.threshold.toLocaleString()} tokens (${status.percentage}%)`); log(``, 'reset'); log(` šŸ“Š MANDATORY: Run pressure check before continuing`, 'red'); log(``, 'reset'); log(` Command:`, 'cyan'); log(` node scripts/check-session-pressure.js --tokens ${currentTokens}/200000 --messages auto`, 'cyan'); log(``, 'reset'); log(` This checkpoint enforces framework discipline and prevents fade.`, 'yellow'); log(``, 'reset'); // Update checkpoints to mark as overdue try { checkpoints.overdue = true; checkpoints.last_check = new Date().toISOString(); fs.writeFileSync(TOKEN_CHECKPOINTS_PATH, JSON.stringify(checkpoints, null, 2)); } catch (err) { // Non-critical } process.exit(1); // BLOCK tool execution } // Checkpoint not overdue - allow execution process.exit(0); } main().catch(err => { error(`Checkpoint validation error: ${err.message}`); process.exit(1); });