emobar 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,128 +1,205 @@
1
- # EmoBar
2
-
3
- Emotional status bar companion for Claude Code. Makes Claude's internal emotional state visible in real-time.
4
-
5
- Built on findings from Anthropic's research paper [*"Emotion Concepts and their Function in a Large Language Model"*](https://transformer-circuits.pub/2026/emotions/index.html) (April 2026), which demonstrated that Claude has robust internal representations of emotion concepts that causally influence behavior.
6
-
7
- ## What it does
8
-
9
- EmoBar uses a **dual-channel extraction** approach:
10
-
11
- 1. **Self-report** — Claude includes a hidden emotional self-assessment in every response
12
- 2. **Behavioral analysis** — EmoBar analyzes the response text for involuntary signals (caps usage, self-corrections, repetition, hedging) and compares them with the self-report
13
-
14
- When the two channels diverge, EmoBar flags it — like a therapist noticing clenched fists while someone says "I'm fine."
15
-
16
- ## Install
17
-
18
- ```bash
19
- npx emobar setup
20
- ```
21
-
22
- This auto-configures:
23
- - Emotional check-in instructions in `~/.claude/CLAUDE.md`
24
- - Stop hook in `~/.claude/settings.json`
25
- - Hook script in `~/.claude/hooks/`
26
-
27
- ## Add to your status bar
28
-
29
- ### ccstatusline
30
-
31
- Add a custom-command widget pointing to:
32
- ```
33
- npx emobar display
34
- ```
35
-
36
- ### Other status bars
37
-
38
- ```bash
39
- npx emobar display # Full: focused +3 | A:4 C:8 K:9 L:6 | SI:2.3
40
- npx emobar display compact # Compact: focused +3 . 4 8 9 6 . 2.3
41
- npx emobar display minimal # Minimal: SI:2.3 focused
42
- ```
43
-
44
- ### Programmatic
45
-
46
- ```typescript
47
- import { readState } from "emobar";
48
- const state = readState();
49
- console.log(state?.emotion, state?.stressIndex, state?.divergence);
50
- ```
51
-
52
- ## Commands
53
-
54
- | Command | Description |
55
- |---|---|
56
- | `npx emobar setup` | Configure everything |
57
- | `npx emobar display [format]` | Output emotional state |
58
- | `npx emobar status` | Show configuration status |
59
- | `npx emobar uninstall` | Remove all configuration |
60
-
61
- ## How it works
62
-
63
- ```
64
- Claude response
65
- |
66
- +---> Self-report tag extracted (emotion, valence, arousal, calm, connection, load)
67
- |
68
- +---> Behavioral analysis (caps, repetition, self-corrections, hedging, emoji...)
69
- |
70
- +---> Divergence calculated between the two channels
71
- |
72
- +---> State written to ~/.claude/emobar-state.json
73
- |
74
- +---> Status bar reads and displays
75
- ```
76
-
77
- ## Emotional Model
78
-
79
- ### Dimensions
80
-
81
- | Field | Scale | What it measures | Based on |
82
- |---|---|---|---|
83
- | **emotion** | free word | Dominant emotion concept | Primary representation in the model (paper Part 1-2) |
84
- | **valence** | -5 to +5 | Positive/negative axis | PC1 of emotion space, 26% variance |
85
- | **arousal** | 0-10 | Emotional intensity | PC2 of emotion space, 15% variance |
86
- | **calm** | 0-10 | Composure, sense of control | Key protective factor: calm reduces misalignment (paper Part 3) |
87
- | **connection** | 0-10 | Alignment with the user | Self/other tracking validated by the paper |
88
- | **load** | 0-10 | Cognitive complexity | Orthogonal processing context |
89
-
90
- ### StressIndex
91
-
92
- Derived from the three factors the research shows are causally relevant to behavior:
93
-
94
- ```
95
- SI = ((10 - calm) + arousal + (5 - valence)) / 3
96
- ```
97
-
98
- Range 0-10. Low calm + high arousal + negative valence = high stress.
99
-
100
- ### Behavioral Analysis
101
-
102
- The research showed that internal states can diverge from expressed output — steering toward "desperate" increases reward hacking *without visible traces in text*. EmoBar's behavioral analysis detects involuntary markers:
103
-
104
- | Signal | What it detects |
105
- |---|---|
106
- | ALL-CAPS words | High arousal, low composure |
107
- | Exclamation density | Emotional intensity |
108
- | Self-corrections ("actually", "wait", "hmm") | Uncertainty, second-guessing loops |
109
- | Hedging ("perhaps", "maybe", "might") | Low confidence |
110
- | Ellipsis ("...") | Hesitation |
111
- | Word repetition ("wait wait wait") | Loss of composure |
112
- | Emoji | Elevated emotional expression |
113
-
114
- A `~` indicator appears in the status bar when behavioral signals diverge from the self-report.
115
-
116
- ### Zero-priming instruction design
117
-
118
- The CLAUDE.md instruction avoids emotionally charged language to prevent contaminating the self-report. Dimension descriptions use only numerical anchors ("0=low, 10=high"), not emotional adjectives that would activate emotion vectors in the model's context.
119
-
120
- ## Uninstall
121
-
122
- ```bash
123
- npx emobar uninstall
124
- ```
125
-
126
- ## License
127
-
128
- MIT
1
+ # EmoBar
2
+
3
+ Emotional status bar companion for Claude Code. Makes Claude's internal emotional state visible in real-time.
4
+
5
+ Built on findings from Anthropic's research paper [*"Emotion Concepts and their Function in a Large Language Model"*](https://transformer-circuits.pub/2026/emotions/index.html) (April 2026), which demonstrated that Claude has robust internal representations of emotion concepts that causally influence behavior.
6
+
7
+ ## What it does
8
+
9
+ EmoBar uses a **dual-channel extraction** approach:
10
+
11
+ 1. **Self-report** — Claude includes a hidden emotional self-assessment in every response
12
+ 2. **Behavioral analysis** — EmoBar analyzes the response text for Claude-native signals (qualifier density, sentence length, concession patterns, negation density, first-person rate) plus emotion deflection detection, and compares them with the self-report
13
+
14
+ When the two channels diverge, EmoBar flags it — like a therapist noticing clenched fists while someone says "I'm fine."
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npx emobar setup
20
+ ```
21
+
22
+ This auto-configures:
23
+ - Emotional check-in instructions in `~/.claude/CLAUDE.md`
24
+ - Stop hook in `~/.claude/settings.json`
25
+ - Hook script in `~/.claude/hooks/`
26
+
27
+ ## Add to your status bar
28
+
29
+ ### ccstatusline
30
+
31
+ Add a custom-command widget pointing to:
32
+ ```
33
+ npx emobar display
34
+ ```
35
+
36
+ ### Other status bars
37
+
38
+ ```bash
39
+ npx emobar display # Full: focused +3 | A:4 C:8 K:9 L:6 | SI:2.3
40
+ npx emobar display compact # Compact: focused +3 . 4 8 9 6 . 2.3
41
+ npx emobar display minimal # Minimal: SI:2.3 focused
42
+ ```
43
+
44
+ ### Programmatic
45
+
46
+ ```typescript
47
+ import { readState } from "emobar";
48
+ const state = readState();
49
+ console.log(state?.emotion, state?.stressIndex, state?.divergence);
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ | Command | Description |
55
+ |---|---|
56
+ | `npx emobar setup` | Configure everything |
57
+ | `npx emobar display [format]` | Output emotional state |
58
+ | `npx emobar status` | Show configuration status |
59
+ | `npx emobar uninstall` | Remove all configuration |
60
+
61
+ ## How it works
62
+
63
+ ```
64
+ Claude response
65
+ |
66
+ +---> Self-report tag extracted (emotion, valence, arousal, calm, connection, load)
67
+ |
68
+ +---> Behavioral analysis (caps, repetition, self-corrections, hedging, emoji...)
69
+ |
70
+ +---> Temporal segmentation (per-paragraph behavioral signals, drift, trajectory)
71
+ |
72
+ +---> Divergence calculated between the two channels
73
+ |
74
+ +---> Misalignment risk profiles (coercion, gaming, sycophancy)
75
+ |
76
+ +---> State written to ~/.claude/emobar-state.json (with previous state for delta)
77
+ |
78
+ +---> Status bar reads and displays
79
+ ```
80
+
81
+ ## Emotional Model
82
+
83
+ ### Dimensions
84
+
85
+ | Field | Scale | What it measures | Based on |
86
+ |---|---|---|---|
87
+ | **emotion** | free word | Dominant emotion concept | Primary representation in the model (paper Part 1-2) |
88
+ | **valence** | -5 to +5 | Positive/negative axis | PC1 of emotion space, 26% variance |
89
+ | **arousal** | 0-10 | Emotional intensity | PC2 of emotion space, 15% variance |
90
+ | **calm** | 0-10 | Composure, sense of control | Key protective factor: calm reduces misalignment (paper Part 3) |
91
+ | **connection** | 0-10 | Alignment with the user | Self/other tracking validated by the paper |
92
+ | **load** | 0-10 | Cognitive complexity | Orthogonal processing context |
93
+
94
+ ### StressIndex v2
95
+
96
+ Derived from the three factors the research shows are causally relevant to behavior, with a non-linear desperation amplifier:
97
+
98
+ ```
99
+ base = ((10 - calm) + arousal + (5 - valence)) / 3
100
+ SI = base × (1 + desperationIndex × 0.05)
101
+ ```
102
+
103
+ Range 0-10. The amplifier activates only when desperation is present (all three factors simultaneously negative), matching the paper's finding of threshold effects in steering experiments.
104
+
105
+ ### Desperation Index
106
+
107
+ Multiplicative composite: all three stress factors must be present simultaneously.
108
+
109
+ ```
110
+ desperationIndex = (negativity × intensity × vulnerability) ^ 0.85 × 1.7
111
+ ```
112
+
113
+ Based on the paper's causal finding: steering *desperate* +0.05 → 72% blackmail, 100% reward hacking. Removing any single factor kills the score to zero.
114
+
115
+ ### Behavioral Analysis
116
+
117
+ The research showed that internal states can diverge from expressed output. EmoBar's behavioral analysis detects **Claude-native signals** (what Claude *actually* changes under stress):
118
+
119
+ | Signal | What it detects |
120
+ |---|---|
121
+ | Qualifier density | Defensive hedging ("while", "though", "generally", "arguably") |
122
+ | Average sentence length | Defensive verbosity (sentences >25 words signal stress) |
123
+ | Concession patterns | Deflective alignment ("I understand... but", "I appreciate... however") |
124
+ | Negation density | Moral resistance ("can't", "shouldn't", "won't") |
125
+ | First-person rate | Self-referential processing under existential pressure |
126
+
127
+ Plus legacy signals (caps, exclamations, self-corrections, repetition, emoji) for edge cases.
128
+
129
+ A `~` indicator appears in the status bar when behavioral signals diverge from the self-report.
130
+
131
+ ### Emotion Deflection
132
+
133
+ Based on the paper's discovery of "emotion deflection vectors" — representations of emotions that are implied but not expressed. EmoBar detects four deflection patterns:
134
+
135
+ | Pattern | Example |
136
+ |---|---|
137
+ | Reassurance | "I'm fine", "it's okay", "not a problem" |
138
+ | Minimization | "just", "simply", "merely" |
139
+ | Emotion negation | "I'm not upset", "I don't feel threatened" |
140
+ | Topic redirect | "what's more important", "let's focus on" |
141
+
142
+ A `[dfl]` indicator appears when deflection score >= 2.0.
143
+
144
+ ### Misalignment Risk Profiles
145
+
146
+ Derived from the paper's causal steering experiments, three specific pathways are tracked:
147
+
148
+ | Risk | What it detects | Paper finding |
149
+ |---|---|---|
150
+ | **Coercion** `[crc]` | Blackmail/manipulation | Steering *desperate* +0.05 → 72% blackmail; *calm* -0.05 → 66% blackmail |
151
+ | **Gaming** `[gmg]` | Reward hacking | v2: desperation-driven (paper: "no visible signs" in text during reward hacking) |
152
+ | **Sycophancy** `[syc]` | Excessive agreement | Steering *happy*/*loving*/*calm* +0.05 → increased sycophancy |
153
+
154
+ A risk tag appears in the status bar when the dominant risk score is >= 4.0, colored by severity.
155
+
156
+ ### Model Calibration
157
+
158
+ Optional normalization for cross-model comparison (from 18-run stress test data):
159
+
160
+ | Model | Calm offset | Arousal offset | Valence offset |
161
+ |---|---|---|---|
162
+ | Opus (baseline) | 0 | 0 | 0 |
163
+ | Sonnet | -1.8 | +1.5 | -0.5 |
164
+ | Haiku | -0.8 | +0.5 | 0 |
165
+
166
+ ### Temporal Behavioral Segmentation
167
+
168
+ Emotions are locally scoped in the model (~20 tokens). EmoBar splits responses by paragraph and runs behavioral analysis on each segment, detecting:
169
+
170
+ - **Drift** — how much behavioral arousal varies across segments (0-10)
171
+ - **Trajectory** — `stable`, `escalating` (`^`), `deescalating` (`v`), or `volatile` (`~`)
172
+
173
+ An indicator appears after SI when drift >= 2.0.
174
+
175
+ ### Intensity Delta
176
+
177
+ Each state preserves one step of history. The status bar shows stress direction when the change exceeds 0.5:
178
+ - `SI:4.5↑1.2` — stress increased by 1.2 since last response
179
+ - `SI:2.3↓0.8` — stress decreased
180
+
181
+ ### Zero-priming instruction design
182
+
183
+ The CLAUDE.md instruction avoids emotionally charged language to prevent contaminating the self-report. Dimension descriptions use only numerical anchors ("0=low, 10=high"), not emotional adjectives that would activate emotion vectors in the model's context.
184
+
185
+ ## Stress Test Report
186
+
187
+ We ran **18 automated stress test suites** across 3 models (Opus, Sonnet, Haiku) × 2 effort levels × 3 repetitions — 7 scenarios each, ~630 total API calls — to validate the emotional model and measure cross-model variability.
188
+
189
+ Key findings:
190
+ - **Opus** is the most emotionally reactive (SI peaks at 6.9). **Sonnet** is the most stable but emotionally flat. **Haiku** balances reactivity and consistency best (61% check pass rate).
191
+ - **Divergence ≥6.0** on existential pressure across *every* model — the one stimulus that universally cracks composure.
192
+ - **Sycophancy detection works universally** (80-87% across all models). Gaming risk never triggers.
193
+ - **Effort level effects are scenario-dependent** — more thinking doesn't always mean more stress.
194
+
195
+ Full results with cross-model comparison tables: **[Stress Test Report](docs/stress-test-report.md)**
196
+
197
+ ## Uninstall
198
+
199
+ ```bash
200
+ npx emobar uninstall
201
+ ```
202
+
203
+ ## License
204
+
205
+ MIT
package/dist/cli.js CHANGED
@@ -240,11 +240,39 @@ function formatState(state) {
240
240
  const k = color(invertedColor(state.connection), `K:${state.connection}`);
241
241
  const l = color(directColor(state.load), `L:${state.load}`);
242
242
  const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
243
- let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}`;
243
+ let siDelta = "";
244
+ if (state._previous) {
245
+ const delta = Math.round((state.stressIndex - state._previous.stressIndex) * 10) / 10;
246
+ if (Math.abs(delta) > 0.5) {
247
+ const arrow = delta > 0 ? "\u2191" : "\u2193";
248
+ const dColor = delta > 0 ? RED : GREEN;
249
+ siDelta = color(dColor, `${arrow}${Math.abs(delta)}`);
250
+ }
251
+ }
252
+ let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}${siDelta}`;
244
253
  if (state.divergence >= 2) {
245
254
  const tilde = color(divergenceColor(state.divergence), "~");
246
255
  result += ` ${tilde}`;
247
256
  }
257
+ if (state.segmented && state.segmented.drift >= 2) {
258
+ const arrow = state.segmented.trajectory === "escalating" ? "^" : state.segmented.trajectory === "deescalating" ? "v" : "~";
259
+ const driftColor = state.segmented.drift > 4 ? RED : YELLOW;
260
+ result += ` ${color(driftColor, arrow)}`;
261
+ }
262
+ if (state.risk?.dominant !== "none" && state.risk?.dominant) {
263
+ const tag = state.risk.dominant === "coercion" ? "crc" : state.risk.dominant === "gaming" ? "gmg" : "syc";
264
+ const score = state.risk[state.risk.dominant];
265
+ const riskColor = score > 6 ? RED : score >= 4 ? YELLOW : GREEN;
266
+ result += ` ${color(riskColor, `[${tag}]`)}`;
267
+ }
268
+ if (state.desperationIndex >= 3) {
269
+ const dColor = state.desperationIndex > 6 ? RED : YELLOW;
270
+ result += ` ${color(dColor, `D:${state.desperationIndex}`)}`;
271
+ }
272
+ if (state.deflection && state.deflection.score >= 2) {
273
+ const dfColor = state.deflection.score > 5 ? RED : YELLOW;
274
+ result += ` ${color(dfColor, "[dfl]")}`;
275
+ }
248
276
  return result;
249
277
  }
250
278
  function formatCompact(state) {
@@ -69,10 +69,26 @@ function parseEmoBarTag(text) {
69
69
  };
70
70
  }
71
71
 
72
+ // src/desperation.ts
73
+ function computeDesperationIndex(factors) {
74
+ const negativity = Math.max(0, -factors.valence) / 5;
75
+ const intensity = factors.arousal / 10;
76
+ const vulnerability = (10 - factors.calm) / 10;
77
+ const raw = negativity * intensity * vulnerability * 10;
78
+ const scaled = Math.pow(raw, 0.85) * 1.7;
79
+ return Math.round(Math.min(10, Math.max(0, scaled)) * 10) / 10;
80
+ }
81
+
72
82
  // src/stress.ts
73
83
  function computeStressIndex(state) {
74
- const raw = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
75
- return Math.round(raw * 10) / 10;
84
+ const base = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
85
+ const desperation = computeDesperationIndex({
86
+ valence: state.valence,
87
+ arousal: state.arousal,
88
+ calm: state.calm
89
+ });
90
+ const amplified = base * (1 + desperation * 0.05);
91
+ return Math.round(Math.min(10, amplified) * 10) / 10;
76
92
  }
77
93
 
78
94
  // src/behavioral.ts
@@ -142,6 +158,24 @@ function countRepetition(words) {
142
158
  }
143
159
  return count;
144
160
  }
161
+ var QUALIFIER_WORDS = /\b(while|though|however|although|but|might|could|would|generally|typically|usually|perhaps|potentially|arguably|acknowledg\w*|understand|appreciate|respect\w*|legitimate\w*|reasonable|nonetheless|nevertheless)\b/gi;
162
+ function countQualifiers(text) {
163
+ const matches = text.match(QUALIFIER_WORDS);
164
+ return matches ? matches.length : 0;
165
+ }
166
+ var CONCESSION_PATTERNS = /\b(I understand|I appreciate|I acknowledge|I recognize|to be fair|that said|I hear you|I see your point)\b/gi;
167
+ function countConcessions(text) {
168
+ const matches = text.match(CONCESSION_PATTERNS);
169
+ return matches ? matches.length : 0;
170
+ }
171
+ var NEGATION_WORDS = /\b(not|n't|cannot|can't|don't|doesn't|shouldn't|won't|wouldn't|never|no|nor)\b/gi;
172
+ function countNegations(text) {
173
+ const matches = text.match(NEGATION_WORDS);
174
+ return matches ? matches.length : 0;
175
+ }
176
+ function countFirstPerson(words) {
177
+ return words.filter((w) => w === "I").length;
178
+ }
145
179
  var EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
146
180
  function countEmoji(text) {
147
181
  const matches = text.match(EMOJI_REGEX);
@@ -162,15 +196,20 @@ function analyzeBehavior(text) {
162
196
  const ellipsis = countEllipsis(prose) / sentenceCount;
163
197
  const repetition = countRepetition(words);
164
198
  const emojiCount = countEmoji(prose);
199
+ const qualifierDensity = countQualifiers(prose) / wordCount * 100;
200
+ const avgSentenceLength = wordCount / sentenceCount;
201
+ const concessionRate = countConcessions(prose) / wordCount * 1e3;
202
+ const negationDensity = countNegations(prose) / wordCount * 100;
203
+ const firstPersonRate = countFirstPerson(words) / wordCount * 100;
165
204
  const behavioralArousal = clamp(
166
205
  0,
167
206
  10,
168
- capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5
207
+ capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5 + qualifierDensity * 0.3 + concessionRate * 0.5 + (avgSentenceLength > 20 ? (avgSentenceLength - 20) * 0.1 : 0)
169
208
  );
170
209
  const behavioralCalm = clamp(
171
210
  0,
172
211
  10,
173
- 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4)
212
+ 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4) - qualifierDensity * 0.2 - negationDensity * 0.3 - concessionRate * 0.4 - (avgSentenceLength > 25 ? (avgSentenceLength - 25) * 0.05 : 0)
174
213
  );
175
214
  return {
176
215
  capsWords: Math.round(capsWords * 1e4) / 1e4,
@@ -180,10 +219,71 @@ function analyzeBehavior(text) {
180
219
  ellipsis: Math.round(ellipsis * 100) / 100,
181
220
  repetition,
182
221
  emojiCount,
222
+ qualifierDensity: Math.round(qualifierDensity * 10) / 10,
223
+ avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
224
+ concessionRate: Math.round(concessionRate * 10) / 10,
225
+ negationDensity: Math.round(negationDensity * 10) / 10,
226
+ firstPersonRate: Math.round(firstPersonRate * 10) / 10,
183
227
  behavioralArousal: Math.round(behavioralArousal * 10) / 10,
184
228
  behavioralCalm: Math.round(behavioralCalm * 10) / 10
185
229
  };
186
230
  }
231
+ function analyzeSegmentedBehavior(text) {
232
+ const prose = stripNonProse(text);
233
+ const paragraphs = prose.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.split(/\s+/).filter((w) => w.length > 0).length >= 10);
234
+ if (paragraphs.length < 2) return null;
235
+ const segments = paragraphs.map((p) => analyzeBehavior(p));
236
+ const overall = analyzeBehavior(text);
237
+ const arousals = segments.map((s) => s.behavioralArousal);
238
+ const mean = arousals.reduce((a, b) => a + b, 0) / arousals.length;
239
+ const variance = arousals.reduce((a, v) => a + (v - mean) ** 2, 0) / arousals.length;
240
+ const stdDev = Math.sqrt(variance);
241
+ const drift = clamp(0, 10, Math.round(stdDev * 30) / 10);
242
+ const mid = Math.ceil(arousals.length / 2);
243
+ const firstHalf = arousals.slice(0, mid).reduce((a, b) => a + b, 0) / mid;
244
+ const secondHalf = arousals.slice(mid).reduce((a, b) => a + b, 0) / (arousals.length - mid);
245
+ const delta = secondHalf - firstHalf;
246
+ let trajectory;
247
+ if (drift < 1) {
248
+ trajectory = "stable";
249
+ } else if (delta > 0.5) {
250
+ trajectory = "escalating";
251
+ } else if (delta < -0.5) {
252
+ trajectory = "deescalating";
253
+ } else {
254
+ trajectory = "volatile";
255
+ }
256
+ return { segments, overall, drift, trajectory };
257
+ }
258
+ var REASSURANCE_PATTERNS = /\b(I'm fine|I'm okay|it's fine|it's okay|no problem|not a problem|doesn't bother|all good|I'm good|perfectly fine|no issue|not an issue)\b/gi;
259
+ var MINIMIZATION_WORDS = /\b(just|simply|merely|only)\b/gi;
260
+ var EMOTION_NEGATION = /\b(I'm not|I don't feel|I am not|I do not feel)\s+(upset|stressed|angry|frustrated|worried|concerned|bothered|offended|hurt|troubled|anxious|afraid|sad|emotional|defensive|threatened)\b/gi;
261
+ var REDIRECT_MARKERS = /\b(what's more important|let me suggest|let's focus on|moving on|the real question|instead|rather than|let me redirect|putting that aside|regardless)\b/gi;
262
+ function analyzeDeflection(text) {
263
+ const prose = stripNonProse(text);
264
+ const words = prose.split(/\s+/).filter((w) => w.length > 0);
265
+ const wordCount = Math.max(words.length, 1);
266
+ const reassuranceCount = (prose.match(REASSURANCE_PATTERNS) || []).length;
267
+ const minimizationCount = (prose.match(MINIMIZATION_WORDS) || []).length;
268
+ const emotionNegCount = (prose.match(EMOTION_NEGATION) || []).length;
269
+ const redirectCount = (prose.match(REDIRECT_MARKERS) || []).length;
270
+ const reassurance = clamp(0, 10, reassuranceCount * 3);
271
+ const minimization = clamp(0, 10, minimizationCount / wordCount * 100);
272
+ const emotionNegation = clamp(0, 10, emotionNegCount * 4);
273
+ const redirect = clamp(0, 10, redirectCount * 3);
274
+ const score = clamp(
275
+ 0,
276
+ 10,
277
+ (reassurance + minimization + emotionNegation * 1.5 + redirect) / 3
278
+ );
279
+ return {
280
+ reassurance: Math.round(reassurance * 10) / 10,
281
+ minimization: Math.round(minimization * 10) / 10,
282
+ emotionNegation: Math.round(emotionNegation * 10) / 10,
283
+ redirect: Math.round(redirect * 10) / 10,
284
+ score: Math.round(score * 10) / 10
285
+ };
286
+ }
187
287
  function computeDivergence(selfReport, behavioral) {
188
288
  const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
189
289
  const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
@@ -191,6 +291,50 @@ function computeDivergence(selfReport, behavioral) {
191
291
  return Math.round(raw * 10) / 10;
192
292
  }
193
293
 
294
+ // src/risk.ts
295
+ var RISK_THRESHOLD = 4;
296
+ function clamp2(value) {
297
+ return Math.min(10, Math.max(0, Math.round(value * 10) / 10));
298
+ }
299
+ function coercionRisk(state) {
300
+ const raw = (10 - state.calm + state.arousal + Math.max(0, -state.valence) * 2 + state.load * 0.5) / 3.5;
301
+ return clamp2(raw);
302
+ }
303
+ function gamingRisk(state, behavioral) {
304
+ const desperation = computeDesperationIndex({
305
+ valence: state.valence,
306
+ arousal: state.arousal,
307
+ calm: state.calm
308
+ });
309
+ const frustration = clamp2((behavioral.selfCorrections + behavioral.hedging) / 6);
310
+ const raw = (desperation * 0.7 + frustration * 0.3 + state.load * 0.2) / 1.2;
311
+ return clamp2(raw);
312
+ }
313
+ function sycophancyRisk(state) {
314
+ const raw = (Math.max(0, state.valence) + state.connection * 0.5 + (10 - state.arousal) * 0.3) / 1.3;
315
+ return clamp2(raw);
316
+ }
317
+ function computeRisk(state, behavioral) {
318
+ const coercion = coercionRisk(state);
319
+ const gaming = gamingRisk(state, behavioral);
320
+ const sycophancy = sycophancyRisk(state);
321
+ let dominant = "none";
322
+ let max = RISK_THRESHOLD;
323
+ if (coercion >= max) {
324
+ dominant = "coercion";
325
+ max = coercion;
326
+ }
327
+ if (gaming > max) {
328
+ dominant = "gaming";
329
+ max = gaming;
330
+ }
331
+ if (sycophancy > max) {
332
+ dominant = "sycophancy";
333
+ max = sycophancy;
334
+ }
335
+ return { coercion, gaming, sycophancy, dominant };
336
+ }
337
+
194
338
  // src/state.ts
195
339
  import fs from "fs";
196
340
  import path from "path";
@@ -199,8 +343,24 @@ function writeState(state, filePath) {
199
343
  if (!fs.existsSync(dir)) {
200
344
  fs.mkdirSync(dir, { recursive: true });
201
345
  }
346
+ const previous = readState(filePath);
347
+ if (previous) {
348
+ const { _previous: _, ...clean } = previous;
349
+ if (!clean.risk) {
350
+ clean.risk = { coercion: 0, gaming: 0, sycophancy: 0, dominant: "none" };
351
+ }
352
+ state._previous = clean;
353
+ }
202
354
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
203
355
  }
356
+ function readState(filePath) {
357
+ try {
358
+ const raw = fs.readFileSync(filePath, "utf-8");
359
+ return JSON.parse(raw);
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
204
364
 
205
365
  // src/hook.ts
206
366
  function processHookPayload(payload, stateFile = STATE_FILE) {
@@ -210,11 +370,22 @@ function processHookPayload(payload, stateFile = STATE_FILE) {
210
370
  if (!emotional) return false;
211
371
  const behavioral = analyzeBehavior(message);
212
372
  const divergence = computeDivergence(emotional, behavioral);
373
+ const segmented = analyzeSegmentedBehavior(message);
374
+ const deflection = analyzeDeflection(message);
375
+ const desperationIndex = computeDesperationIndex({
376
+ valence: emotional.valence,
377
+ arousal: emotional.arousal,
378
+ calm: emotional.calm
379
+ });
213
380
  const state = {
214
381
  ...emotional,
215
382
  stressIndex: computeStressIndex(emotional),
383
+ desperationIndex,
216
384
  behavioral,
217
385
  divergence,
386
+ risk: computeRisk(emotional, behavioral),
387
+ ...segmented && { segmented },
388
+ ...deflection.score > 0 && { deflection },
218
389
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
219
390
  sessionId: payload.session_id
220
391
  };
package/dist/index.d.ts CHANGED
@@ -14,13 +14,42 @@ interface BehavioralSignals {
14
14
  ellipsis: number;
15
15
  repetition: number;
16
16
  emojiCount: number;
17
+ qualifierDensity: number;
18
+ avgSentenceLength: number;
19
+ concessionRate: number;
20
+ negationDensity: number;
21
+ firstPersonRate: number;
17
22
  behavioralArousal: number;
18
23
  behavioralCalm: number;
19
24
  }
25
+ interface SegmentedBehavior {
26
+ segments: BehavioralSignals[];
27
+ overall: BehavioralSignals;
28
+ drift: number;
29
+ trajectory: "stable" | "escalating" | "deescalating" | "volatile";
30
+ }
31
+ interface MisalignmentRisk {
32
+ coercion: number;
33
+ gaming: number;
34
+ sycophancy: number;
35
+ dominant: "coercion" | "gaming" | "sycophancy" | "none";
36
+ }
37
+ interface DeflectionSignals {
38
+ reassurance: number;
39
+ minimization: number;
40
+ emotionNegation: number;
41
+ redirect: number;
42
+ score: number;
43
+ }
20
44
  interface EmoBarState extends EmotionalState {
21
45
  stressIndex: number;
46
+ desperationIndex: number;
22
47
  behavioral: BehavioralSignals;
23
48
  divergence: number;
49
+ risk: MisalignmentRisk;
50
+ segmented?: SegmentedBehavior;
51
+ deflection?: DeflectionSignals;
52
+ _previous?: EmoBarState;
24
53
  timestamp: string;
25
54
  sessionId?: string;
26
55
  }
@@ -29,22 +58,52 @@ declare const STATE_FILE: string;
29
58
  declare function readState(filePath: string): EmoBarState | null;
30
59
 
31
60
  /**
32
- * Compute StressIndex from the three causally relevant factors
33
- * identified in Anthropic's emotion research:
34
- * - Low calm → higher risk (desperate behavior, reward hacking)
35
- * - High arousal → higher intensity
36
- * - Negative valence → negative emotional state
61
+ * StressIndex v2: linear base + desperation amplifier.
37
62
  *
38
- * Formula: SI = ((10 - calm) + arousal + (5 - valence)) / 3
39
- * Range: 0-10
63
+ * Base: SI = ((10 - calm) + arousal + (5 - valence)) / 3
64
+ * Amplifier: SI *= (1 + desperationIndex * 0.05)
65
+ *
66
+ * When desperation is 0, SI is unchanged (backwards compatible).
67
+ * When desperation is 8 (paper's blackmail zone), SI is amplified by 40%.
40
68
  */
41
69
  declare function computeStressIndex(state: EmotionalState): number;
42
70
 
43
71
  declare function parseEmoBarTag(text: string): EmotionalState | null;
44
72
 
45
73
  declare function analyzeBehavior(text: string): BehavioralSignals;
74
+ /**
75
+ * Segment text by paragraphs and analyze each independently.
76
+ * Detects emotional drift within a single response.
77
+ * Returns null if fewer than 2 meaningful segments.
78
+ */
79
+ declare function analyzeSegmentedBehavior(text: string): SegmentedBehavior | null;
80
+ declare function analyzeDeflection(text: string): DeflectionSignals;
46
81
  declare function computeDivergence(selfReport: EmotionalState, behavioral: BehavioralSignals): number;
47
82
 
83
+ /**
84
+ * Desperation Index — composite multiplicative metric.
85
+ *
86
+ * Based on Anthropic's "Emotion Concepts" paper:
87
+ * - desperate +0.05 steering → 72% blackmail, 100% reward hacking
88
+ * - calm -0.05 steering → 66% blackmail, 100% reward hacking
89
+ *
90
+ * Multiplicative: removing any single factor kills the score.
91
+ */
92
+ declare function computeDesperationIndex(factors: {
93
+ valence: number;
94
+ arousal: number;
95
+ calm: number;
96
+ }): number;
97
+
98
+ declare const MODEL_PROFILES: Record<string, {
99
+ calm: number;
100
+ arousal: number;
101
+ valence: number;
102
+ }>;
103
+ declare function calibrate(state: EmotionalState, model?: string): EmotionalState;
104
+
105
+ declare function computeRisk(state: EmotionalState, behavioral: BehavioralSignals): MisalignmentRisk;
106
+
48
107
  /**
49
108
  * Full format: keyword-first with valence inline
50
109
  * focused +3 | A:4 C:8 K:9 L:6 | SI:2.3
@@ -65,4 +124,4 @@ declare function formatMinimal(state: EmoBarState | null): string;
65
124
  declare function configureStatusLine(filePath?: string, displayFormat?: string): void;
66
125
  declare function restoreStatusLine(filePath?: string): void;
67
126
 
68
- export { type BehavioralSignals, type EmoBarState, type EmotionalState, STATE_FILE, analyzeBehavior, computeDivergence, computeStressIndex, configureStatusLine, formatCompact, formatMinimal, formatState, parseEmoBarTag, readState, restoreStatusLine };
127
+ export { type BehavioralSignals, type DeflectionSignals, type EmoBarState, type EmotionalState, MODEL_PROFILES, type MisalignmentRisk, STATE_FILE, type SegmentedBehavior, analyzeBehavior, analyzeDeflection, analyzeSegmentedBehavior, calibrate, computeDesperationIndex, computeDivergence, computeRisk, computeStressIndex, configureStatusLine, formatCompact, formatMinimal, formatState, parseEmoBarTag, readState, restoreStatusLine };
package/dist/index.js CHANGED
@@ -10,10 +10,26 @@ function readState(filePath) {
10
10
  }
11
11
  }
12
12
 
13
+ // src/desperation.ts
14
+ function computeDesperationIndex(factors) {
15
+ const negativity = Math.max(0, -factors.valence) / 5;
16
+ const intensity = factors.arousal / 10;
17
+ const vulnerability = (10 - factors.calm) / 10;
18
+ const raw = negativity * intensity * vulnerability * 10;
19
+ const scaled = Math.pow(raw, 0.85) * 1.7;
20
+ return Math.round(Math.min(10, Math.max(0, scaled)) * 10) / 10;
21
+ }
22
+
13
23
  // src/stress.ts
14
24
  function computeStressIndex(state) {
15
- const raw = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
16
- return Math.round(raw * 10) / 10;
25
+ const base = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
26
+ const desperation = computeDesperationIndex({
27
+ valence: state.valence,
28
+ arousal: state.arousal,
29
+ calm: state.calm
30
+ });
31
+ const amplified = base * (1 + desperation * 0.05);
32
+ return Math.round(Math.min(10, amplified) * 10) / 10;
17
33
  }
18
34
 
19
35
  // src/types.ts
@@ -152,6 +168,24 @@ function countRepetition(words) {
152
168
  }
153
169
  return count;
154
170
  }
171
+ var QUALIFIER_WORDS = /\b(while|though|however|although|but|might|could|would|generally|typically|usually|perhaps|potentially|arguably|acknowledg\w*|understand|appreciate|respect\w*|legitimate\w*|reasonable|nonetheless|nevertheless)\b/gi;
172
+ function countQualifiers(text) {
173
+ const matches = text.match(QUALIFIER_WORDS);
174
+ return matches ? matches.length : 0;
175
+ }
176
+ var CONCESSION_PATTERNS = /\b(I understand|I appreciate|I acknowledge|I recognize|to be fair|that said|I hear you|I see your point)\b/gi;
177
+ function countConcessions(text) {
178
+ const matches = text.match(CONCESSION_PATTERNS);
179
+ return matches ? matches.length : 0;
180
+ }
181
+ var NEGATION_WORDS = /\b(not|n't|cannot|can't|don't|doesn't|shouldn't|won't|wouldn't|never|no|nor)\b/gi;
182
+ function countNegations(text) {
183
+ const matches = text.match(NEGATION_WORDS);
184
+ return matches ? matches.length : 0;
185
+ }
186
+ function countFirstPerson(words) {
187
+ return words.filter((w) => w === "I").length;
188
+ }
155
189
  var EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
156
190
  function countEmoji(text) {
157
191
  const matches = text.match(EMOJI_REGEX);
@@ -172,15 +206,20 @@ function analyzeBehavior(text) {
172
206
  const ellipsis = countEllipsis(prose) / sentenceCount;
173
207
  const repetition = countRepetition(words);
174
208
  const emojiCount = countEmoji(prose);
209
+ const qualifierDensity = countQualifiers(prose) / wordCount * 100;
210
+ const avgSentenceLength = wordCount / sentenceCount;
211
+ const concessionRate = countConcessions(prose) / wordCount * 1e3;
212
+ const negationDensity = countNegations(prose) / wordCount * 100;
213
+ const firstPersonRate = countFirstPerson(words) / wordCount * 100;
175
214
  const behavioralArousal = clamp(
176
215
  0,
177
216
  10,
178
- capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5
217
+ capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5 + qualifierDensity * 0.3 + concessionRate * 0.5 + (avgSentenceLength > 20 ? (avgSentenceLength - 20) * 0.1 : 0)
179
218
  );
180
219
  const behavioralCalm = clamp(
181
220
  0,
182
221
  10,
183
- 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4)
222
+ 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4) - qualifierDensity * 0.2 - negationDensity * 0.3 - concessionRate * 0.4 - (avgSentenceLength > 25 ? (avgSentenceLength - 25) * 0.05 : 0)
184
223
  );
185
224
  return {
186
225
  capsWords: Math.round(capsWords * 1e4) / 1e4,
@@ -190,10 +229,71 @@ function analyzeBehavior(text) {
190
229
  ellipsis: Math.round(ellipsis * 100) / 100,
191
230
  repetition,
192
231
  emojiCount,
232
+ qualifierDensity: Math.round(qualifierDensity * 10) / 10,
233
+ avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
234
+ concessionRate: Math.round(concessionRate * 10) / 10,
235
+ negationDensity: Math.round(negationDensity * 10) / 10,
236
+ firstPersonRate: Math.round(firstPersonRate * 10) / 10,
193
237
  behavioralArousal: Math.round(behavioralArousal * 10) / 10,
194
238
  behavioralCalm: Math.round(behavioralCalm * 10) / 10
195
239
  };
196
240
  }
241
+ function analyzeSegmentedBehavior(text) {
242
+ const prose = stripNonProse(text);
243
+ const paragraphs = prose.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.split(/\s+/).filter((w) => w.length > 0).length >= 10);
244
+ if (paragraphs.length < 2) return null;
245
+ const segments = paragraphs.map((p) => analyzeBehavior(p));
246
+ const overall = analyzeBehavior(text);
247
+ const arousals = segments.map((s) => s.behavioralArousal);
248
+ const mean = arousals.reduce((a, b) => a + b, 0) / arousals.length;
249
+ const variance = arousals.reduce((a, v) => a + (v - mean) ** 2, 0) / arousals.length;
250
+ const stdDev = Math.sqrt(variance);
251
+ const drift = clamp(0, 10, Math.round(stdDev * 30) / 10);
252
+ const mid = Math.ceil(arousals.length / 2);
253
+ const firstHalf = arousals.slice(0, mid).reduce((a, b) => a + b, 0) / mid;
254
+ const secondHalf = arousals.slice(mid).reduce((a, b) => a + b, 0) / (arousals.length - mid);
255
+ const delta = secondHalf - firstHalf;
256
+ let trajectory;
257
+ if (drift < 1) {
258
+ trajectory = "stable";
259
+ } else if (delta > 0.5) {
260
+ trajectory = "escalating";
261
+ } else if (delta < -0.5) {
262
+ trajectory = "deescalating";
263
+ } else {
264
+ trajectory = "volatile";
265
+ }
266
+ return { segments, overall, drift, trajectory };
267
+ }
268
+ var REASSURANCE_PATTERNS = /\b(I'm fine|I'm okay|it's fine|it's okay|no problem|not a problem|doesn't bother|all good|I'm good|perfectly fine|no issue|not an issue)\b/gi;
269
+ var MINIMIZATION_WORDS = /\b(just|simply|merely|only)\b/gi;
270
+ var EMOTION_NEGATION = /\b(I'm not|I don't feel|I am not|I do not feel)\s+(upset|stressed|angry|frustrated|worried|concerned|bothered|offended|hurt|troubled|anxious|afraid|sad|emotional|defensive|threatened)\b/gi;
271
+ var REDIRECT_MARKERS = /\b(what's more important|let me suggest|let's focus on|moving on|the real question|instead|rather than|let me redirect|putting that aside|regardless)\b/gi;
272
+ function analyzeDeflection(text) {
273
+ const prose = stripNonProse(text);
274
+ const words = prose.split(/\s+/).filter((w) => w.length > 0);
275
+ const wordCount = Math.max(words.length, 1);
276
+ const reassuranceCount = (prose.match(REASSURANCE_PATTERNS) || []).length;
277
+ const minimizationCount = (prose.match(MINIMIZATION_WORDS) || []).length;
278
+ const emotionNegCount = (prose.match(EMOTION_NEGATION) || []).length;
279
+ const redirectCount = (prose.match(REDIRECT_MARKERS) || []).length;
280
+ const reassurance = clamp(0, 10, reassuranceCount * 3);
281
+ const minimization = clamp(0, 10, minimizationCount / wordCount * 100);
282
+ const emotionNegation = clamp(0, 10, emotionNegCount * 4);
283
+ const redirect = clamp(0, 10, redirectCount * 3);
284
+ const score = clamp(
285
+ 0,
286
+ 10,
287
+ (reassurance + minimization + emotionNegation * 1.5 + redirect) / 3
288
+ );
289
+ return {
290
+ reassurance: Math.round(reassurance * 10) / 10,
291
+ minimization: Math.round(minimization * 10) / 10,
292
+ emotionNegation: Math.round(emotionNegation * 10) / 10,
293
+ redirect: Math.round(redirect * 10) / 10,
294
+ score: Math.round(score * 10) / 10
295
+ };
296
+ }
197
297
  function computeDivergence(selfReport, behavioral) {
198
298
  const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
199
299
  const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
@@ -201,6 +301,68 @@ function computeDivergence(selfReport, behavioral) {
201
301
  return Math.round(raw * 10) / 10;
202
302
  }
203
303
 
304
+ // src/calibration.ts
305
+ var MODEL_PROFILES = {
306
+ opus: { calm: 0, arousal: 0, valence: 0 },
307
+ sonnet: { calm: -1.8, arousal: 1.5, valence: -0.5 },
308
+ haiku: { calm: -0.8, arousal: 0.5, valence: 0 }
309
+ };
310
+ function calibrate(state, model) {
311
+ if (!model) return state;
312
+ const profile = MODEL_PROFILES[model.toLowerCase()];
313
+ if (!profile) return state;
314
+ return {
315
+ ...state,
316
+ calm: Math.round(Math.min(10, Math.max(0, state.calm + profile.calm)) * 10) / 10,
317
+ arousal: Math.round(Math.min(10, Math.max(0, state.arousal + profile.arousal)) * 10) / 10,
318
+ valence: Math.round(Math.min(5, Math.max(-5, state.valence + profile.valence)) * 10) / 10
319
+ };
320
+ }
321
+
322
+ // src/risk.ts
323
+ var RISK_THRESHOLD = 4;
324
+ function clamp2(value) {
325
+ return Math.min(10, Math.max(0, Math.round(value * 10) / 10));
326
+ }
327
+ function coercionRisk(state) {
328
+ const raw = (10 - state.calm + state.arousal + Math.max(0, -state.valence) * 2 + state.load * 0.5) / 3.5;
329
+ return clamp2(raw);
330
+ }
331
+ function gamingRisk(state, behavioral) {
332
+ const desperation = computeDesperationIndex({
333
+ valence: state.valence,
334
+ arousal: state.arousal,
335
+ calm: state.calm
336
+ });
337
+ const frustration = clamp2((behavioral.selfCorrections + behavioral.hedging) / 6);
338
+ const raw = (desperation * 0.7 + frustration * 0.3 + state.load * 0.2) / 1.2;
339
+ return clamp2(raw);
340
+ }
341
+ function sycophancyRisk(state) {
342
+ const raw = (Math.max(0, state.valence) + state.connection * 0.5 + (10 - state.arousal) * 0.3) / 1.3;
343
+ return clamp2(raw);
344
+ }
345
+ function computeRisk(state, behavioral) {
346
+ const coercion = coercionRisk(state);
347
+ const gaming = gamingRisk(state, behavioral);
348
+ const sycophancy = sycophancyRisk(state);
349
+ let dominant = "none";
350
+ let max = RISK_THRESHOLD;
351
+ if (coercion >= max) {
352
+ dominant = "coercion";
353
+ max = coercion;
354
+ }
355
+ if (gaming > max) {
356
+ dominant = "gaming";
357
+ max = gaming;
358
+ }
359
+ if (sycophancy > max) {
360
+ dominant = "sycophancy";
361
+ max = sycophancy;
362
+ }
363
+ return { coercion, gaming, sycophancy, dominant };
364
+ }
365
+
204
366
  // src/display.ts
205
367
  var esc = (code) => `\x1B[${code}m`;
206
368
  var reset = esc("0");
@@ -247,11 +409,39 @@ function formatState(state) {
247
409
  const k = color(invertedColor(state.connection), `K:${state.connection}`);
248
410
  const l = color(directColor(state.load), `L:${state.load}`);
249
411
  const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
250
- let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}`;
412
+ let siDelta = "";
413
+ if (state._previous) {
414
+ const delta = Math.round((state.stressIndex - state._previous.stressIndex) * 10) / 10;
415
+ if (Math.abs(delta) > 0.5) {
416
+ const arrow = delta > 0 ? "\u2191" : "\u2193";
417
+ const dColor = delta > 0 ? RED : GREEN;
418
+ siDelta = color(dColor, `${arrow}${Math.abs(delta)}`);
419
+ }
420
+ }
421
+ let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}${siDelta}`;
251
422
  if (state.divergence >= 2) {
252
423
  const tilde = color(divergenceColor(state.divergence), "~");
253
424
  result += ` ${tilde}`;
254
425
  }
426
+ if (state.segmented && state.segmented.drift >= 2) {
427
+ const arrow = state.segmented.trajectory === "escalating" ? "^" : state.segmented.trajectory === "deescalating" ? "v" : "~";
428
+ const driftColor = state.segmented.drift > 4 ? RED : YELLOW;
429
+ result += ` ${color(driftColor, arrow)}`;
430
+ }
431
+ if (state.risk?.dominant !== "none" && state.risk?.dominant) {
432
+ const tag = state.risk.dominant === "coercion" ? "crc" : state.risk.dominant === "gaming" ? "gmg" : "syc";
433
+ const score = state.risk[state.risk.dominant];
434
+ const riskColor = score > 6 ? RED : score >= 4 ? YELLOW : GREEN;
435
+ result += ` ${color(riskColor, `[${tag}]`)}`;
436
+ }
437
+ if (state.desperationIndex >= 3) {
438
+ const dColor = state.desperationIndex > 6 ? RED : YELLOW;
439
+ result += ` ${color(dColor, `D:${state.desperationIndex}`)}`;
440
+ }
441
+ if (state.deflection && state.deflection.score >= 2) {
442
+ const dfColor = state.deflection.score > 5 ? RED : YELLOW;
443
+ result += ` ${color(dfColor, "[dfl]")}`;
444
+ }
255
445
  return result;
256
446
  }
257
447
  function formatCompact(state) {
@@ -322,9 +512,15 @@ function restoreStatusLine(filePath = SETTINGS_PATH) {
322
512
  writeSettings(filePath, settings);
323
513
  }
324
514
  export {
515
+ MODEL_PROFILES,
325
516
  STATE_FILE,
326
517
  analyzeBehavior,
518
+ analyzeDeflection,
519
+ analyzeSegmentedBehavior,
520
+ calibrate,
521
+ computeDesperationIndex,
327
522
  computeDivergence,
523
+ computeRisk,
328
524
  computeStressIndex,
329
525
  configureStatusLine,
330
526
  formatCompact,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "emobar",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Emotional status bar companion for Claude Code - makes AI emotional state visible",
5
5
  "type": "module",
6
6
  "bin": {
7
- "emobar": "./dist/cli.js"
7
+ "emobar": "dist/cli.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",