diff --git a/public/admin/hooks-dashboard.html b/public/admin/hooks-dashboard.html
new file mode 100644
index 00000000..60c85b0a
--- /dev/null
+++ b/public/admin/hooks-dashboard.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+
Framework Hooks Dashboard | Tractatus Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Framework Enforcement Metrics
+
Real-time monitoring of Claude Code hook validators and architectural enforcement
+
+
+
+
+
+
+
+
+
+
Total Hook Executions
+
-
+
+
+
+
+
+
+
+
+
+
Operations Blocked
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Executions:
+ -
+
+
+ Blocks:
+ -
+
+
+ Success Rate:
+ -
+
+
+
+
+
+
+
+
+
+
+
+ Total Executions:
+ -
+
+
+ Blocks:
+ -
+
+
+ Success Rate:
+ -
+
+
+
+
+
+
+
+
+
+
Recent Blocked Operations
+
+
+
+
No blocked operations
+
+
+
+
+
+
+
+
Recent Hook Executions
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/admin/hooks-dashboard.js b/public/js/admin/hooks-dashboard.js
new file mode 100644
index 00000000..d5e25ceb
--- /dev/null
+++ b/public/js/admin/hooks-dashboard.js
@@ -0,0 +1,209 @@
+/**
+ * Hooks Dashboard
+ * Real-time monitoring of framework enforcement hooks
+ */
+
+const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:9000/api' : '/api';
+
+// Load metrics on page load
+document.addEventListener('DOMContentLoaded', () => {
+ loadMetrics();
+
+ // Auto-refresh every 30 seconds
+ setInterval(loadMetrics, 30000);
+
+ // Manual refresh button
+ document.getElementById('refresh-btn')?.addEventListener('click', loadMetrics);
+});
+
+/**
+ * Load hook metrics
+ */
+async function loadMetrics() {
+ try {
+ const response = await fetch(`${API_BASE}/admin/hooks/metrics`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('tractatus_admin_token')}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to load metrics');
+ }
+
+ const data = await response.json();
+ displayMetrics(data);
+ } catch (error) {
+ console.error('Error loading metrics:', error);
+ showError('Failed to load hook metrics');
+ }
+}
+
+/**
+ * Display metrics in UI
+ */
+function displayMetrics(data) {
+ const metrics = data.metrics || {};
+ const executions = metrics.hook_executions || [];
+ const blocks = metrics.blocks || [];
+ const stats = metrics.session_stats || {};
+
+ // Calculate totals
+ const totalExecutions = executions.length;
+ const totalBlocks = blocks.length;
+ const blockRate = totalExecutions > 0 ? ((totalBlocks / totalExecutions) * 100).toFixed(1) + '%' : '0%';
+
+ // Update quick stats
+ document.getElementById('stat-total-executions').textContent = totalExecutions.toLocaleString();
+ document.getElementById('stat-total-blocks').textContent = totalBlocks.toLocaleString();
+ document.getElementById('stat-block-rate').textContent = blockRate;
+
+ // Last updated
+ const lastUpdated = stats.last_updated ? formatRelativeTime(new Date(stats.last_updated)) : 'Never';
+ document.getElementById('stat-last-updated').textContent = lastUpdated;
+
+ // Hook breakdown
+ const editExecutions = executions.filter(e => e.hook === 'validate-file-edit').length;
+ const editBlocks = blocks.filter(b => b.hook === 'validate-file-edit').length;
+ const editSuccessRate = editExecutions > 0 ? (((editExecutions - editBlocks) / editExecutions) * 100).toFixed(1) + '%' : '100%';
+
+ document.getElementById('edit-executions').textContent = editExecutions.toLocaleString();
+ document.getElementById('edit-blocks').textContent = editBlocks.toLocaleString();
+ document.getElementById('edit-success-rate').textContent = editSuccessRate;
+
+ const writeExecutions = executions.filter(e => e.hook === 'validate-file-write').length;
+ const writeBlocks = blocks.filter(b => b.hook === 'validate-file-write').length;
+ const writeSuccessRate = writeExecutions > 0 ? (((writeExecutions - writeBlocks) / writeExecutions) * 100).toFixed(1) + '%' : '100%';
+
+ document.getElementById('write-executions').textContent = writeExecutions.toLocaleString();
+ document.getElementById('write-blocks').textContent = writeBlocks.toLocaleString();
+ document.getElementById('write-success-rate').textContent = writeSuccessRate;
+
+ // Recent blocks
+ displayRecentBlocks(blocks.slice(-10).reverse());
+
+ // Recent activity
+ displayRecentActivity(executions.slice(-20).reverse());
+}
+
+/**
+ * Display recent blocked operations
+ */
+function displayRecentBlocks(blocks) {
+ const container = document.getElementById('recent-blocks');
+
+ if (blocks.length === 0) {
+ container.innerHTML = '
No blocked operations
';
+ return;
+ }
+
+ const html = blocks.map(block => `
+
+
+
+
+
+ ${block.hook.replace('validate-file-', '')}
+ ${formatRelativeTime(new Date(block.timestamp))}
+
+
${escapeHtml(block.file)}
+
${escapeHtml(block.reason)}
+
+
+
+ `).join('');
+
+ container.innerHTML = html;
+}
+
+/**
+ * Display recent hook executions
+ */
+function displayRecentActivity(executions) {
+ const container = document.getElementById('recent-activity');
+
+ if (executions.length === 0) {
+ container.innerHTML = '
No recent activity
';
+ return;
+ }
+
+ const html = executions.map(exec => `
+
+
+
+ ${exec.result === 'passed' ? `
+
+ ` : `
+
+ `}
+
+
+
+ ${exec.hook.replace('validate-file-', '')}
+ ${formatRelativeTime(new Date(exec.timestamp))}
+
+
${escapeHtml(exec.file)}
+ ${exec.reason ? `
${escapeHtml(exec.reason)}
` : ''}
+
+
+
+ `).join('');
+
+ container.innerHTML = html;
+}
+
+/**
+ * Format relative time
+ */
+function formatRelativeTime(date) {
+ const now = new Date();
+ const diffMs = now - date;
+ const diffSec = Math.floor(diffMs / 1000);
+ const diffMin = Math.floor(diffSec / 60);
+ const diffHour = Math.floor(diffMin / 60);
+ const diffDay = Math.floor(diffHour / 24);
+
+ if (diffSec < 60) {
+ return 'Just now';
+ } else if (diffMin < 60) {
+ return `${diffMin}m ago`;
+ } else if (diffHour < 24) {
+ return `${diffHour}h ago`;
+ } else if (diffDay < 7) {
+ return `${diffDay}d ago`;
+ } else {
+ return date.toLocaleDateString();
+ }
+}
+
+/**
+ * Escape HTML to prevent XSS
+ */
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+/**
+ * Show error message
+ */
+function showError(message) {
+ const container = document.getElementById('recent-activity');
+ container.innerHTML = `
+
+
${escapeHtml(message)}
+
+
+ `;
+}
diff --git a/scripts/hook-validators/validate-file-edit.js b/scripts/hook-validators/validate-file-edit.js
index 416e45e5..6e16368a 100755
--- a/scripts/hook-validators/validate-file-edit.js
+++ b/scripts/hook-validators/validate-file-edit.js
@@ -23,7 +23,10 @@ const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
-const FILE_PATH = process.argv[2];
+// Hooks receive input via stdin as JSON
+let HOOK_INPUT = null;
+let FILE_PATH = null;
+
const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json');
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.json');
@@ -177,15 +180,99 @@ function updateSessionState() {
}
}
+/**
+ * Log metrics for hook execution
+ */
+function logMetrics(result, reason = null) {
+ try {
+ const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
+ const METRICS_DIR = path.dirname(METRICS_PATH);
+
+ // Ensure directory exists
+ if (!fs.existsSync(METRICS_DIR)) {
+ fs.mkdirSync(METRICS_DIR, { recursive: true });
+ }
+
+ // Load existing metrics
+ let metrics = { hook_executions: [], blocks: [], session_stats: {} };
+ if (fs.existsSync(METRICS_PATH)) {
+ metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
+ }
+
+ // Log execution
+ metrics.hook_executions.push({
+ hook: 'validate-file-edit',
+ timestamp: new Date().toISOString(),
+ file: FILE_PATH,
+ result: result,
+ reason: reason
+ });
+
+ // Log block if failed
+ if (result === 'blocked') {
+ metrics.blocks.push({
+ hook: 'validate-file-edit',
+ timestamp: new Date().toISOString(),
+ file: FILE_PATH,
+ reason: reason
+ });
+ }
+
+ // Update session stats
+ metrics.session_stats.total_edit_hooks = (metrics.session_stats.total_edit_hooks || 0) + 1;
+ metrics.session_stats.total_edit_blocks = (metrics.session_stats.total_edit_blocks || 0) + (result === 'blocked' ? 1 : 0);
+ metrics.session_stats.last_updated = new Date().toISOString();
+
+ // Keep only last 1000 executions
+ if (metrics.hook_executions.length > 1000) {
+ metrics.hook_executions = metrics.hook_executions.slice(-1000);
+ }
+
+ // Keep only last 500 blocks
+ if (metrics.blocks.length > 500) {
+ metrics.blocks = metrics.blocks.slice(-500);
+ }
+
+ // Write metrics
+ fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
+ } catch (err) {
+ // Non-critical - don't fail on metrics logging
+ }
+}
+
+/**
+ * Read hook input from stdin
+ */
+function readHookInput() {
+ return new Promise((resolve) => {
+ let data = '';
+ process.stdin.on('data', chunk => {
+ data += chunk;
+ });
+ process.stdin.on('end', () => {
+ try {
+ resolve(JSON.parse(data));
+ } catch (err) {
+ resolve(null);
+ }
+ });
+ });
+}
+
/**
* Main validation
*/
async function main() {
- if (!FILE_PATH) {
- error('No file path provided');
- process.exit(1);
+ // Read input from stdin
+ HOOK_INPUT = await readHookInput();
+
+ if (!HOOK_INPUT || !HOOK_INPUT.tool_input || !HOOK_INPUT.tool_input.file_path) {
+ error('No file path provided in hook input');
+ logMetrics('error', 'No file path in input');
+ process.exit(2); // Exit code 2 = BLOCK
}
+ FILE_PATH = HOOK_INPUT.tool_input.file_path;
log(`\nš Hook: Validating file edit: ${FILE_PATH}`, 'cyan');
// Check 1: Pre-action validation
@@ -195,7 +282,8 @@ async function main() {
if (preCheck.output) {
console.log(preCheck.output);
}
- process.exit(1);
+ logMetrics('blocked', preCheck.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('Pre-action check passed');
@@ -206,7 +294,8 @@ async function main() {
conflicts.conflicts.forEach(c => {
log(` ⢠${c.id}: ${c.instruction} [${c.quadrant}]`, 'yellow');
});
- process.exit(1);
+ logMetrics('blocked', conflicts.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('No instruction conflicts detected');
@@ -214,18 +303,23 @@ async function main() {
const boundary = checkBoundaryViolation();
if (!boundary.passed) {
error(boundary.reason);
- process.exit(1);
+ logMetrics('blocked', boundary.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('No boundary violations detected');
// Update session state
updateSessionState();
+ // Log successful execution
+ logMetrics('passed');
+
success('File edit validation complete\n');
process.exit(0);
}
main().catch(err => {
error(`Hook validation error: ${err.message}`);
- process.exit(1);
+ logMetrics('error', err.message);
+ process.exit(2); // Exit code 2 = BLOCK
});
diff --git a/scripts/hook-validators/validate-file-write.js b/scripts/hook-validators/validate-file-write.js
index a00eb151..68273788 100755
--- a/scripts/hook-validators/validate-file-write.js
+++ b/scripts/hook-validators/validate-file-write.js
@@ -24,7 +24,10 @@ const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
-const FILE_PATH = process.argv[2];
+// Hooks receive input via stdin as JSON
+let HOOK_INPUT = null;
+let FILE_PATH = null;
+
const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json');
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.json');
@@ -195,12 +198,36 @@ function updateSessionState() {
/**
* Main validation
*/
+/**
+ * Read hook input from stdin
+ */
+function readHookInput() {
+ return new Promise((resolve) => {
+ let data = "";
+ process.stdin.on("data", chunk => {
+ data += chunk;
+ });
+ process.stdin.on("end", () => {
+ try {
+ resolve(JSON.parse(data));
+ } catch (err) {
+ resolve(null);
+ }
+ });
+ });
+}
+
async function main() {
- if (!FILE_PATH) {
- error('No file path provided');
- process.exit(1);
+ // Read input from stdin
+ HOOK_INPUT = await readHookInput();
+
+ if (!HOOK_INPUT || !HOOK_INPUT.tool_input || !HOOK_INPUT.tool_input.file_path) {
+ logMetrics('error', 'No file path in input');
+ error('No file path provided in hook input');
+ process.exit(2); // Exit code 2 = BLOCK
}
+ FILE_PATH = HOOK_INPUT.tool_input.file_path;
log(`\nš Hook: Validating file write: ${FILE_PATH}`, 'cyan');
// Check 1: Pre-action validation
@@ -210,7 +237,8 @@ async function main() {
if (preCheck.output) {
console.log(preCheck.output);
}
- process.exit(1);
+ logMetrics('blocked', preCheck.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('Pre-action check passed');
@@ -218,7 +246,8 @@ async function main() {
const overwriteCheck = checkOverwriteWithoutRead();
if (!overwriteCheck.passed) {
error(overwriteCheck.reason);
- process.exit(1);
+ logMetrics('blocked', overwriteCheck.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
// Check 3: CrossReferenceValidator
@@ -228,7 +257,8 @@ async function main() {
conflicts.conflicts.forEach(c => {
log(` ⢠${c.id}: ${c.instruction} [${c.quadrant}]`, 'yellow');
});
- process.exit(1);
+ logMetrics('blocked', conflicts.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('No instruction conflicts detected');
@@ -236,18 +266,67 @@ async function main() {
const boundary = checkBoundaryViolation();
if (!boundary.passed) {
error(boundary.reason);
- process.exit(1);
+ logMetrics('blocked', boundary.reason);
+ process.exit(2); // Exit code 2 = BLOCK
}
success('No boundary violations detected');
// Update session state
updateSessionState();
+ // Log successful execution
+ logMetrics('passed');
+
success('File write validation complete\n');
process.exit(0);
}
main().catch(err => {
error(`Hook validation error: ${err.message}`);
- process.exit(1);
+ logMetrics('error', err.message);
+ process.exit(2); // Exit code 2 = BLOCK
});
+
+/**
+ * Log metrics for hook execution
+ */
+function logMetrics(result, reason = null) {
+ try {
+ const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
+ const METRICS_DIR = path.dirname(METRICS_PATH);
+ if (!fs.existsSync(METRICS_DIR)) {
+ fs.mkdirSync(METRICS_DIR, { recursive: true });
+ }
+ let metrics = { hook_executions: [], blocks: [], session_stats: {} };
+ if (fs.existsSync(METRICS_PATH)) {
+ metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
+ }
+ metrics.hook_executions.push({
+ hook: 'validate-file-write',
+ timestamp: new Date().toISOString(),
+ file: FILE_PATH,
+ result: result,
+ reason: reason
+ });
+ if (result === 'blocked') {
+ metrics.blocks.push({
+ hook: 'validate-file-write',
+ timestamp: new Date().toISOString(),
+ file: FILE_PATH,
+ reason: reason
+ });
+ }
+ metrics.session_stats.total_write_hooks = (metrics.session_stats.total_write_hooks || 0) + 1;
+ metrics.session_stats.total_write_blocks = (metrics.session_stats.total_write_blocks || 0) + (result === 'blocked' ? 1 : 0);
+ metrics.session_stats.last_updated = new Date().toISOString();
+ if (metrics.hook_executions.length > 1000) {
+ metrics.hook_executions = metrics.hook_executions.slice(-1000);
+ }
+ if (metrics.blocks.length > 500) {
+ metrics.blocks = metrics.blocks.slice(-500);
+ }
+ fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
+ } catch (err) {
+ // Non-critical
+ }
+}
diff --git a/src/routes/hooks-metrics.routes.js b/src/routes/hooks-metrics.routes.js
new file mode 100644
index 00000000..acc3325b
--- /dev/null
+++ b/src/routes/hooks-metrics.routes.js
@@ -0,0 +1,49 @@
+/**
+ * Hooks Metrics API Routes
+ * Serves framework enforcement metrics to admin dashboard
+ */
+
+const express = require('express');
+const router = express.Router();
+const fs = require('fs');
+const path = require('path');
+const { authMiddleware, roleMiddleware } = require('../middleware/auth.middleware');
+
+const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
+
+/**
+ * GET /api/admin/hooks/metrics
+ * Get current hooks metrics
+ */
+router.get('/metrics', authMiddleware, roleMiddleware(['admin']), async (req, res) => {
+ try {
+ // Check if metrics file exists
+ if (!fs.existsSync(METRICS_PATH)) {
+ return res.json({
+ success: true,
+ metrics: {
+ hook_executions: [],
+ blocks: [],
+ session_stats: {}
+ }
+ });
+ }
+
+ // Read metrics
+ const metricsData = fs.readFileSync(METRICS_PATH, 'utf8');
+ const metrics = JSON.parse(metricsData);
+
+ res.json({
+ success: true,
+ metrics: metrics
+ });
+ } catch (error) {
+ console.error('Error fetching hooks metrics:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to fetch hooks metrics'
+ });
+ }
+});
+
+module.exports = router;
diff --git a/src/routes/index.js b/src/routes/index.js
index 62cfd51b..3c261723 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -14,6 +14,7 @@ const newsletterRoutes = require('./newsletter.routes');
const mediaRoutes = require('./media.routes');
const casesRoutes = require('./cases.routes');
const adminRoutes = require('./admin.routes');
+const hooksMetricsRoutes = require('./hooks-metrics.routes');
const rulesRoutes = require('./rules.routes');
const projectsRoutes = require('./projects.routes');
const auditRoutes = require('./audit.routes');
@@ -35,6 +36,7 @@ router.use('/newsletter', newsletterRoutes);
router.use('/media', mediaRoutes);
router.use('/cases', casesRoutes);
router.use('/admin', adminRoutes);
+router.use('/admin/hooks', hooksMetricsRoutes);
router.use('/admin/rules', rulesRoutes);
router.use('/admin/projects', projectsRoutes);
router.use('/admin', auditRoutes);