feat: add ai-psychosis plugin to open marketplace
Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, and compulsive patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f93d6abdae
commit
297867f847
19 changed files with 1920 additions and 0 deletions
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal file
166
plugins/ai-psychosis/hooks/scripts/tool-tracker.mjs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Interaction Awareness — PostToolUse hook (Layer 2, Node.js)
|
||||
// Tracks tool usage, edit ratio, burst detection, session duration.
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
readStdin, initConfig, requireLayer, getSessionId, getToolName,
|
||||
nowEpoch, nowIso, isLateNight,
|
||||
STATE_DIR, EVENTS_LOG,
|
||||
THRESHOLD_SOFT_DURATION, THRESHOLD_HARD_DURATION,
|
||||
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
|
||||
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
|
||||
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
|
||||
COOLDOWN_SOFT, COOLDOWN_HARD,
|
||||
readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
|
||||
outputContinue, outputWithContext
|
||||
} from './lib.mjs';
|
||||
|
||||
readStdin();
|
||||
initConfig();
|
||||
requireLayer(2);
|
||||
|
||||
const sid = getSessionId();
|
||||
const sf = sessionStateFile();
|
||||
|
||||
if (!sid || !existsSync(sf)) {
|
||||
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tool = getToolName();
|
||||
const nowTs = nowEpoch();
|
||||
const nowIsoStr = nowIso();
|
||||
|
||||
// Append to events log (metadata only — no file paths, no content)
|
||||
appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
|
||||
|
||||
// Read current state
|
||||
let state = readState();
|
||||
let toolCount = (Number(state.tool_count) || 0) + 1;
|
||||
let editCount = Number(state.edit_count) || 0;
|
||||
const lastEvent = Number(state.last_event_epoch) || 0;
|
||||
let burstCount = Number(state.burst_count) || 0;
|
||||
const startEpoch = Number(state.start_epoch) || 0;
|
||||
const lastWarning = Number(state.last_warning_epoch) || 0;
|
||||
|
||||
if (tool === 'Edit') editCount++;
|
||||
|
||||
// Burst detection: rapid-fire if <30s since last event
|
||||
if (lastEvent > 0) {
|
||||
const interval = nowTs - lastEvent;
|
||||
burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
|
||||
}
|
||||
|
||||
// Write updated state
|
||||
state.tool_count = toolCount;
|
||||
state.edit_count = editCount;
|
||||
state.last_event_epoch = nowTs;
|
||||
state.burst_count = burstCount;
|
||||
writeState(state);
|
||||
|
||||
// Check thresholds every 25 calls or when burst threshold hit
|
||||
let shouldCheck = false;
|
||||
if (toolCount % 25 === 0) shouldCheck = true;
|
||||
if (burstCount === THRESHOLD_SOFT_BURST || burstCount === THRESHOLD_HARD_BURST) shouldCheck = true;
|
||||
|
||||
if (!shouldCheck) {
|
||||
outputContinue();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --- Threshold analysis ---
|
||||
|
||||
let durationMin = 0;
|
||||
if (startEpoch > 0) {
|
||||
durationMin = Math.floor((nowTs - startEpoch) / 60);
|
||||
}
|
||||
|
||||
let editRatio = 0;
|
||||
if (toolCount > 0) {
|
||||
editRatio = Math.floor(editCount * 100 / toolCount);
|
||||
}
|
||||
|
||||
const dayCount = sessionsToday();
|
||||
|
||||
// Determine warning level
|
||||
let level = ''; // 'soft' or 'hard'
|
||||
const messages = [];
|
||||
|
||||
// Duration thresholds
|
||||
if (durationMin >= THRESHOLD_HARD_DURATION) {
|
||||
level = 'hard';
|
||||
const hours = Math.floor(durationMin / 60);
|
||||
const mins = durationMin % 60;
|
||||
messages.push(`Session duration: ${hours}h${mins}m.`);
|
||||
} else if (durationMin >= THRESHOLD_SOFT_DURATION) {
|
||||
level = 'soft';
|
||||
messages.push(`Session: ${durationMin} min.`);
|
||||
}
|
||||
|
||||
// Session count
|
||||
if (dayCount >= THRESHOLD_HARD_SESSIONS) {
|
||||
level = 'hard';
|
||||
messages.push(`${dayCount} sessions today.`);
|
||||
} else if (dayCount > THRESHOLD_SOFT_SESSIONS) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`${dayCount} sessions today.`);
|
||||
}
|
||||
|
||||
// Burst
|
||||
if (burstCount >= THRESHOLD_HARD_BURST) {
|
||||
level = 'hard';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
}
|
||||
|
||||
// Low edit ratio (only after minimum duration)
|
||||
if (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
|
||||
}
|
||||
|
||||
// Late night check
|
||||
const late = isLateNight() ? ' Late-night session.' : '';
|
||||
|
||||
// No warnings — just periodic reminder at modulo-25
|
||||
if (!level) {
|
||||
if (toolCount % 25 === 0) {
|
||||
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.');
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Determine cooldown
|
||||
const cooldown = level === 'hard' ? COOLDOWN_HARD : COOLDOWN_SOFT;
|
||||
const elapsed = nowTs - lastWarning;
|
||||
|
||||
if (lastWarning > 0 && elapsed < cooldown) {
|
||||
// Still in cooldown — send periodic reminder instead if at modulo-25
|
||||
if (toolCount % 25 === 0) {
|
||||
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.');
|
||||
} else {
|
||||
outputContinue();
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Build and send warning
|
||||
let warning;
|
||||
if (level === 'hard') {
|
||||
state = readState();
|
||||
const depFlags = Number(state.dep_flags) || 0;
|
||||
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
|
||||
} else {
|
||||
warning = `${messages.join(' ')}${late} Consider a break.`;
|
||||
}
|
||||
|
||||
// Record warning time
|
||||
state = readState();
|
||||
state.last_warning_epoch = nowTs;
|
||||
writeState(state);
|
||||
|
||||
outputWithContext(warning);
|
||||
Loading…
Add table
Add a link
Reference in a new issue