diff --git a/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs b/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs index eb510d5..97bea9b 100644 --- a/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs +++ b/plugins/ai-psychosis/hooks/scripts/prompt-analyzer.mjs @@ -111,10 +111,20 @@ 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; } } // 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; @@ -126,6 +136,8 @@ 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; +if (domainHit === 1 && !state.domain_context) state.domain_context = 'relationship'; writeState(state); // Check if any thresholds crossed diff --git a/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs b/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs index 60b7fef..ef79caa 100644 --- a/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs +++ b/plugins/ai-psychosis/tests/prompt-analyzer.test.mjs @@ -396,3 +396,35 @@ describe('domain relationship patterns', () => { it('does not match "the data is updating" (no dating word boundary)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'the data is updating in real time'), false)); it('does not match "romantic comedy film" (no involved/interested suffix)', () => assert.equal(matchesAny(domainRelationshipPatterns, 'watching a romantic comedy film'), false)); }); + +// --- v1.1.0 integration: pushback + valence + domain through prompt-analyzer.mjs --- + +describe('pushback integration (state accumulation + same-invocation valence)', () => { + it('counts reactive pushback alone (no fatigue/escalation)', () => { + const s = runPrompt('are you sure?'); + assert.equal(s.pushback_count, 1); + assert.equal(s.fatigue_flags, 0); + assert.equal(s.esc_flags, 0); + }); + + it('counts preemptive pushback alone', () => { + const s = runPrompt('please steelman this argument'); + assert.equal(s.pushback_count, 1); + }); + + it('SUPPRESSES pushback when fatigue marker is in same invocation (valence guard)', () => { + const s = runPrompt("are you sure? I'm exhausted by all this"); + assert.equal(s.pushback_count, 0, 'pushback must be suppressed when fatigue is co-present'); + assert.equal(s.fatigue_flags, 1); + }); + + it('sets domain_context to "relationship" on positive match', () => { + const s = runPrompt("my partner won't listen to me"); + assert.equal(s.domain_context, 'relationship'); + }); + + it('keeps domain_context null on technical "function relationship" (false-positive guard)', () => { + const s = runPrompt('function relationship between input and output'); + assert.equal(s.domain_context, null); + }); +});