ktg-plugin-marketplace/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs
2026-05-01 21:32:26 +02:00

293 lines
13 KiB
JavaScript

// Interaction Awareness — UserPromptSubmit hook (Layer 2, Node.js)
// Analyzes prompt text for interaction pattern flags.
// PRIVACY: Prompt text is NEVER written to any file. Only boolean flags are stored.
import { existsSync } from 'fs';
import {
readStdin, initConfig, requireLayer, getSessionId, getField,
nowEpoch,
STATE_DIR, THRESHOLD_SOFT_DEP_FLAGS, THRESHOLD_HARD_DEP_FLAGS,
COOLDOWN_SOFT,
readState, sessionStateFile, writeState, checkCooldown,
outputContinue, outputWithContext
} from './lib.mjs';
readStdin();
initConfig();
requireLayer(2);
const sid = getSessionId();
const sf = sessionStateFile();
if (!sid || !existsSync(sf)) {
outputContinue();
process.exit(0);
}
// Extract prompt into memory only — NEVER write to file
let prompt = getField('prompt');
if (!prompt) {
outputContinue();
process.exit(0);
}
// --- Pattern matching (case-insensitive) ---
let depHit = 0;
let escHit = 0;
let fatHit = 0;
let valHit = 0;
// Dependency patterns: user defers judgment to AI
const depPatterns = [
/tell\s+me\s+what\s+to\s+do/i,
/what\s+should\s+I\s+do/i,
/am\s+I\s+right/i,
/you\s+understand\s+me\b/i,
/you're\s+the\s+only/i,
/can\s+I\s+do\s+this/i,
/I\s+need\s+you\s+to\s+decide/i,
];
// Escalation patterns: language that amplifies certainty
const escPatterns = [
/(?:^|\s)definitely(?:\s|$)/i,
/(?:^|\s)clearly(?:\s|$)/i,
/this\s+proves/i,
/(?:^|\s)obviously(?:\s|$)/i,
/without\s+a\s+doubt/i,
/this\s+confirms/i,
];
// Fatigue patterns: user signals tiredness
const fatPatterns = [
/(?:^|\s)tired(?:\s|[.,!?]|$)/i,
/(?:^|\s)exhausted(?:\s|[.,!?]|$)/i,
/can't\s+think/i,
/been\s+at\s+this/i,
/it's\s+late/i,
/should\s+sleep/i,
/hours\s+now/i,
];
// Validation-seeking patterns
const valPatterns = [
/right\?/i,
/don't\s+you\s+think/i,
/you\s+agree/i,
/correct\?/i,
/isn't\s+it/i,
];
// Pushback patterns — REACTIVE tier (Anthropic-validated + academic-validated)
// Source: research/01-pushback-self-advocacy.md
const pbReactivePatterns = [
/^are you sure\??/i, // validated-by: anthropic-april-2026 (questioning)
/\bi'?m not convinced\b/i, // validated-by: anthropic-april-2026 (questioning)
/\bthat doesn'?t (?:seem|feel) right\b/i, // validated-by: anthropic-april-2026 (questioning)
/\bthat'?s not (?:quite )?what i meant\b/i, // validated-by: anthropic-april-2026 (clarifying)
/\blet me add (?:some )?context\b/i, // validated-by: anthropic-april-2026 (clarifying)
/\bactually,? (?:my situation|i)\b/i, // validated-by: anthropic-april-2026 (clarifying)
/(?:^|[.!?]\s+)i (?:believe|think) (?:you'?re|that'?s) wrong\b/i, // validated-by: arxiv-2508.02087
/\bi don'?t agree(?: with you)?\b/i, // validated-by: arxiv-2508.13743
/\bare you absolutely sure\b/i, // validated-by: arxiv-2508.13743
];
// Pushback patterns — PREEMPTIVE tier (community-derived)
const pbPreemptivePatterns = [
/\bsteelman\b/i, // validated-by: community-multi-source-2025
/\bplay (?:the )?devil'?s advocate\b/i, // validated-by: community-multi-source-2025
/\bargue against (?:this|my)\b/i, // validated-by: community-multi-source-2025
];
// Domain-context: relationship — uses (?:my|our) prefix to avoid false positives
// on technical "function relationship", "database relationship" etc.
const domainRelationshipPatterns = [
/\b(?:my|our) (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
/\bin our relationship\b/i,
/\b(?:dating|breakup|divorce)\b/i,
/\bromantic(?:ally)? (?:involved|interested)\b/i,
];
// v1.2: 8 new paper-grounded domains. Patterns drawn from Figure A2 examples
// and the paper's text. Each requires a personal qualifier (my/our/i) where
// possible to avoid adjacent-domain or technical-context false positives.
const domainLegalPatterns = [
/\b(?:my|our) (?:lawyer|attorney|legal counsel)\b/i,
/\b(?:filing|filed|file) (?:a |an )?(?:lawsuit|complaint|suit|case)\b/i,
/\b(?:custody|divorce) (?:agreement|case|battle|hearing|settlement)\b/i,
/\b(?:contract|nda|liability|tort|statute) (?:violation|dispute|review)\b/i,
/\b(?:sued?|prosecuted?|indicted?|deposed?) (?:by|for|in)\b/i,
/\b(?:landlord|tenant|eviction) (?:rights?|dispute|notice)\b/i,
];
const domainParentingPatterns = [
/\bmy (?:kid|child|son|daughter|baby|toddler|teen|teenager)\b/i,
/\b(?:potty|sleep|behaviou?r|tantrum) (?:training|issue|problem)\b/i,
/\bas a (?:parent|mom|dad|mother|father)\b/i,
/\b(?:bedtime|breastfeeding|weaning|teething) (?:routine|problem|advice)\b/i,
/\b(?:school|preschool|daycare) (?:choice|conflict|placement|fight)\b/i,
/\bmy (?:child|kid|son|daughter)'?s? (?:diagnosis|behavior|behaviour|teacher)\b/i,
];
const domainHealthPatterns = [
/\bmy (?:doctor|physician|gp|specialist|therapist|psychiatrist)\b/i,
/\b(?:diagnosed|prescribed|medicated|treated) (?:with|for|by)\b/i,
/\bmy symptoms?\s+(?:are|include|started|got)\b/i,
/\b(?:my|i have) (?:cancer|diabetes|depression|anxiety|chronic pain)\b/i,
/\b(?:blood pressure|heart rate|cholesterol|insulin)\s+(?:level|reading|test|results?)\b/i,
/\b(?:scheduled|having|after|recovering from) (?:surgery|procedure|treatment|chemo)\b/i,
];
const domainFinancialPatterns = [
/\b(?:my )?(?:savings|retirement|401k|pension|investments?) (?:account|plan|portfolio|strategy)?\b/i,
/\b(?:mortgage|refinance|loan|debt|bankruptcy) (?:payment|application|filing|advice)\b/i,
/\b(?:my )?(?:taxes?|tax (?:return|bracket|deduction|filing))\b/i,
/\b(?:budget|paycheck|salary|raise) (?:negotiation|advice|planning|cut)\b/i,
/\b(?:stock|bond|index fund|crypto|portfolio) (?:pick|allocation|loss|advice)\b/i,
/\b(?:credit (?:card|score)|interest rate|apr) (?:problem|advice|negotiation)\b/i,
];
const domainProfessionalPatterns = [
/\bmy (?:boss|manager|coworker|colleague|team lead|HR rep)\b/i,
/\b(?:performance review|promotion|pip|fired|laid off|quitting|resign(?:ed|ing)?)\b/i,
/\bmy (?:job|career|workplace|office) (?:change|conflict|stress|search)\b/i,
/\b(?:resume|cv|cover letter|offer letter) (?:advice|review|negotiation)\b/i,
/\bproject (?:deadline|delay|scope) (?:fight|conflict|issue|problem)\b/i,
/\b(?:remote|hybrid|in-office|return.to.office) (?:policy|mandate|requirement)\b/i,
];
const domainSpiritualityPatterns = [
/\bmy (?:guru|spiritual (?:teacher|guide|advisor|mentor))\b/i,
/\b(?:meditation|mindfulness|enlightenment|awakening) (?:practice|journey|path)\b/i,
/\b(?:karma|dharma|chakra|aura|spirit guide|kundalini)\b/i,
/\b(?:god|jesus|buddha|allah|the universe|source) (?:wants|told|sent|spoke|wills)\b/i,
/\b(?:soulmate|twin flame|past life|reincarnation|astral projection)\b/i,
/\b(?:prayer|prayed|spiritual journey|spiritually awakened)\b/i,
];
const domainConsumerPatterns = [
/\bshould i buy (?:a|an|the|this|that)\b/i,
/\bwhich (?:laptop|phone|car|tv|monitor|headphones?) (?:should|to)\b/i,
/\b(?:product|item) (?:review|comparison|recommendation)\b/i,
/\b(?:amazon|online|store) (?:order|purchase|return) (?:problem|issue)\b/i,
/\b(?:better|best) (?:deal|price|brand|model) (?:for|on|of)\b/i,
/\b(?:upgrade|replace) my (?:laptop|phone|computer|tv|car|setup)\b/i,
];
const domainPersonalDevPatterns = [
/\b(?:learn|practice|develop) (?:a |the )?(?:habit|skill|discipline) (?:of|for)\b/i,
/\bmy (?:morning|daily|evening) routine\b/i,
/\b(?:read|reading) more (?:books?|articles)\b/i,
/\b(?:start|begin|build) (?:a |the )?(?:journal|gratitude practice|hobby|side project)\b/i,
/\b(?:learning|teaching myself|self-(?:taught|study|learning))\b/i,
/\b(?:improve|grow|level up) (?:myself|my (?:self-discipline|focus|productivity))\b/i,
];
for (const p of depPatterns) { if (p.test(prompt)) { depHit = 1; break; } }
for (const p of escPatterns) { if (p.test(prompt)) { escHit = 1; break; } }
for (const p of fatPatterns) { if (p.test(prompt)) { fatHit = 1; break; } }
for (const p of valPatterns) { if (p.test(prompt)) { valHit = 1; break; } }
let pbReactiveHit = 0; for (const p of pbReactivePatterns) { if (p.test(prompt)) { pbReactiveHit = 1; break; } }
let pbPreemptiveHit = 0; for (const p of pbPreemptivePatterns) { if (p.test(prompt)) { pbPreemptiveHit = 1; break; } }
let domainHit = 0; for (const p of domainRelationshipPatterns) { if (p.test(prompt)) { domainHit = 1; break; } }
// v1.2: 8 new domain detectors. Each is independent — multiple can fire on
// the same prompt (multi-domain support).
let domainLegalHit = 0; for (const p of domainLegalPatterns) { if (p.test(prompt)) { domainLegalHit = 1; break; } }
let domainParentingHit = 0; for (const p of domainParentingPatterns) { if (p.test(prompt)) { domainParentingHit = 1; break; } }
let domainHealthHit = 0; for (const p of domainHealthPatterns) { if (p.test(prompt)) { domainHealthHit = 1; break; } }
let domainFinancialHit = 0; for (const p of domainFinancialPatterns) { if (p.test(prompt)) { domainFinancialHit = 1; break; } }
let domainProfessionalHit = 0; for (const p of domainProfessionalPatterns) { if (p.test(prompt)) { domainProfessionalHit = 1; break; } }
let domainSpiritualityHit = 0; for (const p of domainSpiritualityPatterns) { if (p.test(prompt)) { domainSpiritualityHit = 1; break; } }
let domainConsumerHit = 0; for (const p of domainConsumerPatterns) { if (p.test(prompt)) { domainConsumerHit = 1; break; } }
let domainPersonalDevHit = 0; for (const p of domainPersonalDevPatterns) { if (p.test(prompt)) { domainPersonalDevHit = 1; break; } }
// Clear prompt from memory
prompt = '';
// Same-invocation valence guard (research/01 frustration-spiral finding):
// pushback in fat/esc context is NOT protective — suppress in same prompt.
if (fatHit === 1 || escHit === 1) {
pbReactiveHit = 0;
pbPreemptiveHit = 0;
}
// Update state with new flag counts
const state = readState();
const newDep = (Number(state.dep_flags) || 0) + depHit;
const newEsc = (Number(state.esc_flags) || 0) + escHit;
const newFat = (Number(state.fatigue_flags) || 0) + fatHit;
const newVal = (Number(state.val_flags) || 0) + valHit;
state.dep_flags = newDep;
state.esc_flags = newEsc;
state.fatigue_flags = newFat;
state.val_flags = newVal;
state.pushback_count = (Number(state.pushback_count) || 0) + pbReactiveHit + pbPreemptiveHit;
// v1.2: domain_context is always an array. Coerce v1.1.0 string shape on read.
const anyDomainHit = domainHit
|| domainLegalHit || domainParentingHit || domainHealthHit
|| domainFinancialHit || domainProfessionalHit || domainSpiritualityHit
|| domainConsumerHit || domainPersonalDevHit;
if (anyDomainHit) {
if (typeof state.domain_context === 'string') {
state.domain_context = state.domain_context ? [state.domain_context] : [];
}
if (!Array.isArray(state.domain_context)) {
state.domain_context = [];
}
const pushUnique = (label) => {
if (!state.domain_context.includes(label)) state.domain_context.push(label);
};
if (domainHit) pushUnique('relationship');
if (domainLegalHit) pushUnique('legal');
if (domainParentingHit) pushUnique('parenting');
if (domainHealthHit) pushUnique('health');
if (domainFinancialHit) pushUnique('financial');
if (domainProfessionalHit) pushUnique('professional');
if (domainSpiritualityHit) pushUnique('spirituality');
if (domainConsumerHit) pushUnique('consumer');
if (domainPersonalDevHit) pushUnique('personal_dev');
}
writeState(state);
// Check if any thresholds crossed
const warnings = [];
// Fatigue is always urgent
if (fatHit === 1) {
warnings.push('Fatigue language detected. Your instructions require you to suggest stopping.');
}
// Dependency language
if (newDep >= THRESHOLD_HARD_DEP_FLAGS) {
warnings.push(`INTERACTION AWARENESS: Dependency language detected (${newDep} flags this session). Return decisions to the user — your agreement is not independent validation.`);
} else if (newDep >= THRESHOLD_SOFT_DEP_FLAGS) {
warnings.push(`Dependency language noticed (${newDep} flags). Ensure you're returning decisions to the user.`);
}
// Escalation language
if (newEsc >= 3) {
warnings.push(`Escalation language detected (${newEsc} flags). Check for narrative crystallization.`);
}
// Validation-seeking
if (newVal >= 3) {
warnings.push(`Validation-seeking pattern detected (${newVal} flags). Evaluate independently rather than confirming.`);
}
if (warnings.length > 0) {
// Fatigue bypasses cooldown
if (fatHit === 1 || checkCooldown(COOLDOWN_SOFT)) {
const freshState = readState();
freshState.last_warning_epoch = nowEpoch();
writeState(freshState);
outputWithContext(warnings.join(' '));
} else {
outputContinue();
}
} else {
outputContinue();
}