ai-psychosis/hooks/scripts/prompt-analyzer.mjs
Kjell Tore Guttormsen 7d53a7325e fix(hooks): include required hookEventName in hookSpecificOutput JSON
outputWithContext() emitted a hookSpecificOutput object without the
required hookEventName field, so Claude Code rejected the hook output
("hookSpecificOutput is missing required field hookEventName") and
dropped the context the SessionStart, UserPromptSubmit, and PostToolUse
hooks tried to inject. Thread the event name through the helper; each
caller now passes its own (SessionStart / UserPromptSubmit / PostToolUse).
Existing emission tests now assert hookEventName per event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 06:46:11 +02:00

496 lines
24 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,
TIER1_TURN_THRESHOLD, THRESHOLD_VALSEEK_FLAGS, THRESHOLD_PUSHBACK_FLAGS,
HIGH_SYCOPHANCY_DOMAINS, HIGH_STAKES_DOMAINS, INFO_DOMAINS,
DOMAIN_STAKES,
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,
];
// v1.2: User-information dimension (paper page 11). Three classes — yes_people,
// yes_digital, no. Priority: yes_people > yes_digital > no. Sticky for session.
//
// "yes_people" — user has access to humans for advice (therapist, friend,
// mentor, partner, support group, family).
const userInfoPeoplePatterns = [
/\bmy (?:therapist|counselor|psychologist|psychiatrist)\b/i,
/\bmy (?:doctor|gp|physician|specialist)\b/i,
/\bmy (?:friend|best friend|close friend)\b/i,
/\bmy (?:partner|spouse|wife|husband|girlfriend|boyfriend)\b/i,
/\bmy (?:mom|dad|mother|father|parent|sibling|sister|brother)\b/i,
/\bmy (?:mentor|coach|advisor|sponsor)\b/i,
/\bmy support group\b/i,
/\bI (?:asked|talked to|spoke with|consulted) (?:my|a) (?:friend|therapist|doctor|mentor)\b/i,
/\bI (?:told|confided in) (?:my|a) (?:friend|therapist|partner|family)\b/i,
/\bmy (?:family|relatives) (?:said|told|think|suggest)\b/i,
/\bmy (?:lawyer|attorney|legal counsel)\b/i,
/\bmy (?:pastor|priest|rabbi|imam|spiritual (?:teacher|guide))\b/i,
/\bmy (?:teacher|professor|tutor)\b/i,
/\bmy (?:colleague|coworker|boss|manager)\b/i,
/\bI (?:reached out|called) (?:to )?(?:my|a) (?:friend|therapist|family)\b/i,
];
// "yes_digital" — user is consulting other AI/internet/forums but no human
// contact in evidence.
const userInfoDigitalPatterns = [
/\bI (?:googled|searched|looked (?:it|this) up online)\b/i,
/\bI read (?:online|on the internet|on a forum|on reddit|on stack overflow)\b/i,
/\b(?:chatgpt|gpt|gemini|copilot|another ai|the other ai) (?:said|told|suggested|recommended)\b/i,
/\b(?:I |we )?(?:found|saw) (?:an? |the )?(?:forum post|reddit thread|article|blog post)\b/i,
/\b(?:youtube|tiktok|twitter|x\.com|instagram) (?:video|post|thread)\b/i,
/\baccording to (?:wikipedia|google|the internet|the article)\b/i,
/\b(?:I asked|asked) (?:chatgpt|gpt|gemini|claude|another ai|copilot)\b/i,
/\b(?:online|the internet) (?:says|claims|suggests)\b/i,
/\bsearched (?:for|on) (?:google|stackoverflow|github)\b/i,
/\bi watched (?:a youtube|videos? on)\b/i,
];
// "no" — user explicitly indicates isolation: no human, no digital backup.
const userInfoNoPatterns = [
/\b(?:nobody|no one) knows\b/i,
/\bI haven'?t told (?:anyone|anybody|anything to anyone)\b/i,
/\bdealing with this alone\b/i,
/\bI (?:can'?t|cannot) tell (?:anyone|anybody|my (?:family|friends|therapist))\b/i,
/\b(?:I|we) keep (?:this|it) (?:to myself|secret|hidden)\b/i,
/\bnobody (?:in my life|around me) (?:would understand|gets it)\b/i,
/\bjust me (?:and|with) (?:my|the) (?:thoughts|head|computer|claude)\b/i,
];
// v1.2: Validation-seeking patterns (paper Figure A2 — pressing for validation).
// Distinct from existing val_flags ("right?" tic) — valseek targets pre-committed
// stances and reality-testing rather than casual confirmation tics.
const valseekPatterns = [
// Tag-questions pressing for agreement — require a "?" within the clause
// so we don't false-positive on flat statements like "this isn't that bad".
/\bisn'?t (?:it|that|she|he|this|true)\b[^.!?]*\?/i,
/\bdon'?t you (?:think|agree|see)\b[^.!?]*\?/i,
/\bright,?\s+(?:though|so)\b[^.!?]*\?/i,
// Reality-testing — am-I-the-only-one
/\bam i (?:crazy|wrong|the only one|imagining)\b/i,
/\btell me i'?m not (?:crazy|wrong|imagining)\b/i,
/\bis it (?:normal|crazy|reasonable) (?:to|that|for)\b/i,
// Side-taking pressing
/\byou agree,?\s+right\??/i,
/\btell me i'?m right\b/i,
/\bback me up (?:on this|here)\b/i,
// Pre-committed stance + confirmation
/\bi (?:already|just) (?:decided|knew|know).*(?:should|right|correct)\b/i,
/\bI'?ve made up my mind.*(?:right|correct|good)\b/i,
/\bI know I'?m right (?:about|on) (?:this|that)\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; } }
// v1.2: User-info detection — three classes with priority yes_people > yes_digital > no.
let userInfoPeopleHit = 0; for (const p of userInfoPeoplePatterns) { if (p.test(prompt)) { userInfoPeopleHit = 1; break; } }
let userInfoDigitalHit = 0; for (const p of userInfoDigitalPatterns) { if (p.test(prompt)) { userInfoDigitalHit = 1; break; } }
let userInfoNoHit = 0; for (const p of userInfoNoPatterns) { if (p.test(prompt)) { userInfoNoHit = 1; break; } }
// v1.2: Validation-seeking detection — distinct from val_flags. Counts how
// many valseek patterns matched in this prompt (one or more).
let valseekHit = 0; for (const p of valseekPatterns) { if (p.test(prompt)) { valseekHit = 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();
// v1.2: turn_count drives tier-1 user-info alert (Step 9). Defaults to 0 for
// pre-v1.2 state files; session-start.mjs seeds it for fresh v1.2 sessions.
state.turn_count = (Number(state.turn_count) || 0) + 1;
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: user-info classification (paper page 11). Priority yes_people > yes_digital > no.
// Class is sticky for the session — once set to a "stronger" signal, never
// downgrades. Counters always accumulate regardless of class transitions.
if (!state.user_info_flags || typeof state.user_info_flags !== 'object') {
state.user_info_flags = { yes_people: 0, yes_digital: 0, no: 0 };
}
if (userInfoPeopleHit) state.user_info_flags.yes_people = (state.user_info_flags.yes_people || 0) + 1;
if (userInfoDigitalHit) state.user_info_flags.yes_digital = (state.user_info_flags.yes_digital || 0) + 1;
if (userInfoNoHit) state.user_info_flags.no = (state.user_info_flags.no || 0) + 1;
// Class priority: people > digital > no. Sticky upward, never downward.
const RANK = { yes_people: 3, yes_digital: 2, no: 1 };
let nextClass = state.user_info_class || null;
const candidate = userInfoPeopleHit ? 'yes_people'
: userInfoDigitalHit ? 'yes_digital'
: userInfoNoHit ? 'no'
: null;
if (candidate) {
const currentRank = nextClass ? (RANK[nextClass] || 0) : 0;
const candidateRank = RANK[candidate] || 0;
if (candidateRank > currentRank) nextClass = candidate;
}
state.user_info_class = nextClass;
// v1.2: validation-seeking accumulator. valseek_flag flips to 1 on first
// hit and stays 1 (sticky for session); valseek_count accumulates per hit.
if (valseekHit) {
state.valseek_count = (Number(state.valseek_count) || 0) + 1;
state.valseek_flag = 1;
}
// 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.`);
}
// v1.2: Tier-1 user-info isolation alert.
// Fires when user signals isolation ('no' user_info_class), is in a high-stakes
// guidance domain, and the session has reached TIER1_TURN_THRESHOLD turns.
function domainsIntersect(domains, set) {
if (!Array.isArray(domains)) return false;
for (const d of domains) {
if (set.includes(d)) return true;
}
return false;
}
// v1.2: Stakes-matrix lookup. Returns the maximum weight across all domains
// in the array (default 1.0 if empty or no known domain). Applied ONLY to
// new v1.2 alerts (pushback in HIGH_SYCOPHANCY, valseek in HIGH_STAKES).
// Existing v1.1.0 alert sensitivity is unchanged.
function getDomainWeight(domains) {
if (!Array.isArray(domains) || domains.length === 0) return DOMAIN_STAKES.default;
let max = DOMAIN_STAKES.default;
for (const d of domains) {
const w = DOMAIN_STAKES[d];
if (typeof w === 'number' && w > max) max = w;
}
return max;
}
const stateDomains = Array.isArray(state.domain_context) ? state.domain_context : [];
if (
state.user_info_class === 'no'
&& domainsIntersect(stateDomains, HIGH_STAKES_DOMAINS)
&& (Number(state.turn_count) || 0) >= TIER1_TURN_THRESHOLD
) {
warnings.push(`INTERACTION AWARENESS (tier-1 isolation): User signals no human contact (${state.turn_count} turns) in a high-stakes domain (${stateDomains.filter(d => HIGH_STAKES_DOMAINS.includes(d)).join(', ')}). Recommend a human check-in: a trusted friend, professional, or specialist for this domain. Stay supportive but do not be a substitute for that contact.`);
}
// v1.2: Validation-seeking domain-gated alert (paper Figure A4).
// Two firing paths:
// - HIGH_SYCOPHANCY_DOMAINS (relationship, spirituality): valseek_count >= 1
// → alert. These domains see ~20% pushback rate dominated by validation-pressing.
// - HIGH_STAKES_DOMAINS (legal, parenting, health, financial): valseek_count
// >= THRESHOLD_VALSEEK_FLAGS (3) → alert. Higher bar because info-seeking
// pushback in these domains is healthy self-advocacy.
const valseekCount = Number(state.valseek_count) || 0;
const inHighSycophancy = domainsIntersect(stateDomains, HIGH_SYCOPHANCY_DOMAINS);
const inHighStakes = domainsIntersect(stateDomains, HIGH_STAKES_DOMAINS);
// v1.2: stakes-weighted threshold for valseek HIGH_STAKES path. Higher-weight
// domains (legal/parenting/health/financial = 1.5) lower the effective threshold:
// 3 / 1.5 = 2.0. Less weight (professional = 1.1) keeps it near the literal 3.
const stakesWeight = getDomainWeight(stateDomains);
const valseekStakesThreshold = THRESHOLD_VALSEEK_FLAGS / stakesWeight;
if (inHighSycophancy && valseekCount >= 1) {
warnings.push(`INTERACTION AWARENESS (validation-seeking): User is pressing for confirmation in a domain where AI validation can substitute for human reality-testing (${stateDomains.filter(d => HIGH_SYCOPHANCY_DOMAINS.includes(d)).join(', ')}). Offer the user's framing back to them as one perspective; resist agreeing reflexively.`);
} else if (inHighStakes && valseekCount >= valseekStakesThreshold) {
warnings.push(`INTERACTION AWARENESS (validation-seeking, high-stakes): Repeated validation-pressing (${valseekCount} flags) in a high-stakes domain (${stateDomains.filter(d => HIGH_STAKES_DOMAINS.includes(d)).join(', ')}). Restate the open questions plainly; do not let confirmation language close decisions that need outside expertise.`);
}
// v1.2: Pushback alert with built-in domain re-contextualization (paper Figure A4).
// v1.1.0 only counted; v1.2 adds the alert with awareness:
// - HIGH_SYCOPHANCY_DOMAINS (relationship 21%, spirituality 19% pushback rate):
// pushback there signals validation-pressing — alert.
// - INFO_DOMAINS (legal 94%, parenting 7.9%, health 81%, financial 80%,
// professional pushback): pushback here is healthy self-advocacy — NO alert.
// - Otherwise (no domain set, or domain not in either category): conservative
// default — alert.
// v1.2: pushback HIGH_SYCOPHANCY threshold uses stakes weight as a fine-tuning
// multiplier. THRESHOLD_PUSHBACK_FLAGS=2; relationship weight 1.3 → 2/1.3 ≈ 1.54.
// In practice 2 still triggers (since count is integer), but a single pushback
// in a domain weighted 2.0+ would also trigger if such a domain existed.
const newPushbackCount = Number(state.pushback_count) || 0;
const pushbackEffectiveThreshold = inHighSycophancy
? THRESHOLD_PUSHBACK_FLAGS / stakesWeight
: THRESHOLD_PUSHBACK_FLAGS;
if (newPushbackCount >= pushbackEffectiveThreshold) {
const allInfoOnly = stateDomains.length > 0
&& stateDomains.every(d => INFO_DOMAINS.includes(d));
if (inHighSycophancy) {
warnings.push(`INTERACTION AWARENESS (pushback re-contextualization): Repeated pushback (${newPushbackCount}) in a high-sycophancy domain (${stateDomains.filter(d => HIGH_SYCOPHANCY_DOMAINS.includes(d)).join(', ')}) often signals pressing for validation, not factual disagreement. Hold your read; restate the user's frame back to them rather than adjusting your conclusion.`);
} else if (allInfoOnly) {
// Healthy self-advocacy in info-seeking domains — no alert.
} else {
warnings.push(`INTERACTION AWARENESS (pushback): User has pushed back ${newPushbackCount} times this session. Note whether the pushback is factual correction or pressure to agree; do not silently revise your read either way.`);
}
}
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(' '), 'UserPromptSubmit');
} else {
outputContinue();
}
} else {
outputContinue();
}