- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
249 lines
7.1 KiB
JavaScript
Executable file
249 lines
7.1 KiB
JavaScript
Executable file
#!/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');
|
|
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load and display HIGH persistence instructions
|
|
* Helps refresh critical instructions at checkpoints
|
|
*/
|
|
function displayCriticalInstructions() {
|
|
try {
|
|
if (!fs.existsSync(INSTRUCTION_HISTORY_PATH)) {
|
|
return;
|
|
}
|
|
|
|
const history = JSON.parse(fs.readFileSync(INSTRUCTION_HISTORY_PATH, 'utf8'));
|
|
const activeInstructions = history.instructions?.filter(i => i.active) || [];
|
|
|
|
// Filter for HIGH persistence instructions
|
|
const highPersistence = activeInstructions.filter(i => i.persistence === 'HIGH');
|
|
|
|
// Filter for STRATEGIC and SYSTEM quadrants
|
|
const strategic = highPersistence.filter(i => i.quadrant === 'STRATEGIC');
|
|
const system = highPersistence.filter(i => i.quadrant === 'SYSTEM');
|
|
|
|
if (highPersistence.length === 0) {
|
|
return;
|
|
}
|
|
|
|
log(``, 'reset');
|
|
log(` 📋 INSTRUCTION PERSISTENCE REFRESH`, 'cyan');
|
|
log(` ─────────────────────────────────────────────────────────────`, 'cyan');
|
|
log(``, 'reset');
|
|
log(` At token checkpoints, HIGH persistence instructions must be validated:`, 'yellow');
|
|
log(``, 'reset');
|
|
|
|
// Display STRATEGIC instructions (values, ethics)
|
|
if (strategic.length > 0) {
|
|
log(` 🎯 STRATEGIC (Values & Ethics) - ${strategic.length} active:`, 'cyan');
|
|
strategic.slice(0, 5).forEach((inst, idx) => {
|
|
const preview = inst.instruction.length > 70
|
|
? inst.instruction.substring(0, 67) + '...'
|
|
: inst.instruction;
|
|
log(` ${idx + 1}. [${inst.id}] ${preview}`, 'reset');
|
|
});
|
|
if (strategic.length > 5) {
|
|
log(` ... and ${strategic.length - 5} more`, 'yellow');
|
|
}
|
|
log(``, 'reset');
|
|
}
|
|
|
|
// Display SYSTEM instructions (architectural constraints)
|
|
if (system.length > 0) {
|
|
log(` ⚙️ SYSTEM (Architectural Constraints) - ${system.length} active:`, 'cyan');
|
|
system.slice(0, 5).forEach((inst, idx) => {
|
|
const preview = inst.instruction.length > 70
|
|
? inst.instruction.substring(0, 67) + '...'
|
|
: inst.instruction;
|
|
log(` ${idx + 1}. [${inst.id}] ${preview}`, 'reset');
|
|
});
|
|
if (system.length > 5) {
|
|
log(` ... and ${system.length - 5} more`, 'yellow');
|
|
}
|
|
log(``, 'reset');
|
|
}
|
|
|
|
log(` 💡 These instructions remain active and must be cross-referenced`, 'yellow');
|
|
log(` before conflicting actions.`, 'yellow');
|
|
log(``, 'reset');
|
|
|
|
} catch (err) {
|
|
// Non-critical - don't fail checkpoint if instruction display fails
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
|
|
// Display critical instructions to refresh working memory
|
|
displayCriticalInstructions();
|
|
|
|
// 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);
|
|
});
|