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 +205 -128
- package/dist/cli.js +29 -1
- package/dist/emobar-hook.js +175 -4
- package/dist/index.d.ts +67 -8
- package/dist/index.js +201 -5
- package/package.json +2 -2
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
|
|
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
|
-
+--->
|
|
71
|
-
|
|
|
72
|
-
+--->
|
|
73
|
-
|
|
|
74
|
-
+--->
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
87
|
-
| **
|
|
88
|
-
| **
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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) {
|
package/dist/emobar-hook.js
CHANGED
|
@@ -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
|
|
75
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
|
16
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
7
|
+
"emobar": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|