@usejarvis/brain 0.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.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Struggle Detector — Behavioral Analysis
3
+ *
4
+ * Detects when the user is actively working but making no progress:
5
+ * trial-and-error editing, repeated failing commands, undo cycles.
6
+ * Complements stuck detection (which only fires when screen is unchanging).
7
+ */
8
+
9
+ // ── Types ──
10
+
11
+ export type AppCategory =
12
+ | 'code_editor'
13
+ | 'terminal'
14
+ | 'browser'
15
+ | 'creative_app'
16
+ | 'puzzle_game'
17
+ | 'general';
18
+
19
+ export type StruggleSignal = {
20
+ name: string;
21
+ score: number; // 0.0 - 1.0
22
+ detail: string;
23
+ };
24
+
25
+ export type StruggleAnalysis = {
26
+ isStruggling: boolean;
27
+ compositeScore: number;
28
+ signals: StruggleSignal[];
29
+ appCategory: AppCategory;
30
+ appName: string;
31
+ windowTitle: string;
32
+ durationMs: number;
33
+ };
34
+
35
+ type Snapshot = {
36
+ timestamp: number;
37
+ ocrHash: string;
38
+ ocrText: string;
39
+ appName: string;
40
+ outputHash: string; // hash of bottom 500 chars (terminal/compiler output area)
41
+ };
42
+
43
+ // ── Constants ──
44
+
45
+ const MAX_SNAPSHOTS = 30; // ~3.5 min at 7s intervals
46
+ const WINDOW_MS = 3.5 * 60 * 1000; // 3.5 minutes
47
+ const MIN_SNAPSHOTS = 8; // ~56s of data before analysis
48
+
49
+ const SIGNAL_WEIGHTS = {
50
+ trialAndError: 0.30,
51
+ undoRevert: 0.25,
52
+ repeatedOutput: 0.25,
53
+ lowProgress: 0.20,
54
+ };
55
+
56
+ const STRUGGLE_THRESHOLD = 0.5;
57
+
58
+ // ── App Classification Patterns ──
59
+
60
+ const CODE_EDITORS = /\b(VS\s?Code|Visual Studio Code|IntelliJ|WebStorm|PyCharm|CLion|GoLand|RubyMine|Sublime|Atom|vim|nvim|neovim|Emacs|Cursor|Zed|nano|Code - OSS|code-oss)\b/i;
61
+ const TERMINALS = /\b(Terminal|iTerm|Konsole|Alacritty|Warp|kitty|GNOME Terminal|Windows Terminal|PowerShell|pwsh|cmd\.exe|bash|zsh|tmux|screen|Hyper)\b/i;
62
+ const BROWSERS = /\b(Chrome|Chromium|Firefox|Brave|Edge|Safari|Opera|Arc|Vivaldi)\b/i;
63
+ const CREATIVE_APPS = /\b(Photoshop|Figma|GIMP|Blender|Illustrator|Inkscape|Sketch|Affinity|Canva|Lightroom|Premiere|DaVinci|After Effects|Krita|Paint\.NET)\b/i;
64
+ const PUZZLE_INDICATORS = /\b(score|level|moves|timer|puzzle|sudoku|wordle|crossword|chess|2048|minesweeper|solitaire|tetris)\b/i;
65
+
66
+ // ── Main Class ──
67
+
68
+ export class StruggleDetector {
69
+ private snapshots: Snapshot[] = [];
70
+ private lastStruggleEmitAt = 0;
71
+ private struggleStartedAt: number | null = null;
72
+ private graceMs: number;
73
+ private cooldownMs: number;
74
+
75
+ constructor(opts?: { graceMs?: number; cooldownMs?: number }) {
76
+ this.graceMs = opts?.graceMs ?? 120_000;
77
+ this.cooldownMs = opts?.cooldownMs ?? 180_000;
78
+ }
79
+
80
+ /**
81
+ * Evaluate current capture for struggle patterns.
82
+ * Returns StruggleAnalysis when struggle is confirmed (past grace + cooldown), null otherwise.
83
+ */
84
+ evaluate(ocrText: string, appName: string, windowTitle: string, timestamp: number): StruggleAnalysis | null {
85
+ const ocrHash = simpleHash(ocrText);
86
+ const outputHash = simpleHash(ocrText.slice(-500));
87
+
88
+ this.snapshots.push({ timestamp, ocrHash, ocrText, appName, outputHash });
89
+
90
+ // Evict old entries
91
+ const cutoff = timestamp - WINDOW_MS;
92
+ this.snapshots = this.snapshots.filter(s => s.timestamp > cutoff);
93
+ if (this.snapshots.length > MAX_SNAPSHOTS) {
94
+ this.snapshots = this.snapshots.slice(-MAX_SNAPSHOTS);
95
+ }
96
+
97
+ // Need enough data
98
+ if (this.snapshots.length < MIN_SNAPSHOTS) return null;
99
+
100
+ // All snapshots must be from the same app (struggle resets on app switch)
101
+ if (!this.snapshots.every(s => s.appName === appName)) return null;
102
+
103
+ // Compute signals
104
+ const signals: StruggleSignal[] = [];
105
+
106
+ const trialAndError = this.detectTrialAndError();
107
+ signals.push(trialAndError);
108
+
109
+ const undoRevert = this.detectUndoRevert();
110
+ signals.push(undoRevert);
111
+
112
+ const repeatedOutput = this.detectRepeatedOutput();
113
+ signals.push(repeatedOutput);
114
+
115
+ const lowProgress = this.detectLowProgress();
116
+ signals.push(lowProgress);
117
+
118
+ // Composite score
119
+ const compositeScore =
120
+ trialAndError.score * SIGNAL_WEIGHTS.trialAndError +
121
+ undoRevert.score * SIGNAL_WEIGHTS.undoRevert +
122
+ repeatedOutput.score * SIGNAL_WEIGHTS.repeatedOutput +
123
+ lowProgress.score * SIGNAL_WEIGHTS.lowProgress;
124
+
125
+ if (compositeScore < STRUGGLE_THRESHOLD) {
126
+ this.struggleStartedAt = null;
127
+ return null;
128
+ }
129
+
130
+ // Grace period: don't fire immediately
131
+ if (this.struggleStartedAt === null) {
132
+ this.struggleStartedAt = timestamp;
133
+ return null;
134
+ }
135
+ if (timestamp - this.struggleStartedAt < this.graceMs) {
136
+ return null;
137
+ }
138
+
139
+ // Cooldown: don't fire too often
140
+ if (timestamp - this.lastStruggleEmitAt < this.cooldownMs) {
141
+ return null;
142
+ }
143
+
144
+ // Fire!
145
+ this.lastStruggleEmitAt = timestamp;
146
+ this.struggleStartedAt = null;
147
+
148
+ const appCategory = this.classifyApp(appName, windowTitle, ocrText);
149
+ const durationMs = timestamp - this.snapshots[0]!.timestamp;
150
+
151
+ return {
152
+ isStruggling: true,
153
+ compositeScore,
154
+ signals,
155
+ appCategory,
156
+ appName,
157
+ windowTitle,
158
+ durationMs,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Classify the current app into a category for prompt selection.
164
+ */
165
+ classifyApp(appName: string, windowTitle: string, ocrText: string): AppCategory {
166
+ const combined = `${appName} ${windowTitle}`;
167
+
168
+ if (CODE_EDITORS.test(combined)) return 'code_editor';
169
+ if (TERMINALS.test(combined)) return 'terminal';
170
+ if (CREATIVE_APPS.test(combined)) return 'creative_app';
171
+ if (BROWSERS.test(combined)) return 'browser';
172
+
173
+ // Puzzle game detection: check window title + OCR heuristics
174
+ if (PUZZLE_INDICATORS.test(combined) || PUZZLE_INDICATORS.test(ocrText.slice(0, 500))) {
175
+ return 'puzzle_game';
176
+ }
177
+
178
+ return 'general';
179
+ }
180
+
181
+ /**
182
+ * Reset state — call on app switch.
183
+ */
184
+ reset(): void {
185
+ this.snapshots = [];
186
+ this.struggleStartedAt = null;
187
+ }
188
+
189
+ // ── Signal Detectors ──
190
+
191
+ /**
192
+ * Signal 1: Trial-and-error editing.
193
+ * High ratio of unique OCR hashes = many small changes that don't converge.
194
+ */
195
+ private detectTrialAndError(): StruggleSignal {
196
+ const total = this.snapshots.length;
197
+ const uniqueHashes = new Set(this.snapshots.map(s => s.ocrHash)).size;
198
+ const ratio = uniqueHashes / total;
199
+
200
+ // If > 70% of captures are unique AND we have enough unique ones,
201
+ // the user is making lots of small changes without settling
202
+ if (uniqueHashes >= 10 && ratio > 0.7) {
203
+ const score = Math.min(1.0, (ratio - 0.5) * 3);
204
+ return {
205
+ name: 'trial_and_error',
206
+ score,
207
+ detail: `${uniqueHashes}/${total} unique screen states (${Math.round(ratio * 100)}% unique)`,
208
+ };
209
+ }
210
+
211
+ return { name: 'trial_and_error', score: 0, detail: 'Normal editing pattern' };
212
+ }
213
+
214
+ /**
215
+ * Signal 2: Undo/revert patterns.
216
+ * Detects when current OCR hash matches a hash from 2-5 snapshots ago (text reverted).
217
+ */
218
+ private detectUndoRevert(): StruggleSignal {
219
+ let revertCount = 0;
220
+
221
+ for (let i = 2; i < this.snapshots.length; i++) {
222
+ const current = this.snapshots[i]!.ocrHash;
223
+ // Check if current matches any of the 2-5 snapshots before it
224
+ for (let k = 2; k <= Math.min(5, i); k++) {
225
+ if (current === this.snapshots[i - k]!.ocrHash) {
226
+ revertCount++;
227
+ break; // count each revert once
228
+ }
229
+ }
230
+ }
231
+
232
+ if (revertCount >= 2) {
233
+ const score = Math.min(1.0, revertCount / 5);
234
+ return {
235
+ name: 'undo_revert',
236
+ score,
237
+ detail: `${revertCount} revert cycles detected (text returning to previous states)`,
238
+ };
239
+ }
240
+
241
+ return { name: 'undo_revert', score: 0, detail: 'No revert patterns' };
242
+ }
243
+
244
+ /**
245
+ * Signal 3: Repeated output.
246
+ * Bottom of screen (terminal/compiler output) stays the same across many captures.
247
+ */
248
+ private detectRepeatedOutput(): StruggleSignal {
249
+ const outputHashes = this.snapshots.map(s => s.outputHash);
250
+ const counts = new Map<string, number>();
251
+
252
+ for (const h of outputHashes) {
253
+ counts.set(h, (counts.get(h) ?? 0) + 1);
254
+ }
255
+
256
+ // Find most repeated output hash
257
+ let maxCount = 0;
258
+ for (const count of counts.values()) {
259
+ maxCount = Math.max(maxCount, count);
260
+ }
261
+
262
+ const pct = maxCount / this.snapshots.length;
263
+
264
+ if (pct > 0.4 && maxCount >= 6) {
265
+ const score = Math.min(1.0, (pct - 0.3) / 0.4);
266
+ return {
267
+ name: 'repeated_output',
268
+ score,
269
+ detail: `Same output in ${maxCount}/${this.snapshots.length} captures (${Math.round(pct * 100)}%)`,
270
+ };
271
+ }
272
+
273
+ return { name: 'repeated_output', score: 0, detail: 'Output is changing normally' };
274
+ }
275
+
276
+ /**
277
+ * Signal 4: Low meaningful progress.
278
+ * Small average edit distance between consecutive snapshots despite many captures.
279
+ */
280
+ private detectLowProgress(): StruggleSignal {
281
+ if (this.snapshots.length < 3) {
282
+ return { name: 'low_progress', score: 0, detail: 'Not enough data' };
283
+ }
284
+
285
+ let totalDist = 0;
286
+ let comparisons = 0;
287
+
288
+ for (let i = 1; i < this.snapshots.length; i++) {
289
+ const dist = cheapEditDistance(
290
+ this.snapshots[i - 1]!.ocrText,
291
+ this.snapshots[i]!.ocrText
292
+ );
293
+ totalDist += dist;
294
+ comparisons++;
295
+ }
296
+
297
+ const avgDist = comparisons > 0 ? totalDist / comparisons : 0;
298
+
299
+ // If average edit distance is small (< 100 chars difference per capture),
300
+ // the user is making minor tweaks without significant progress
301
+ if (avgDist > 0 && avgDist < 100) {
302
+ const score = Math.min(1.0, 1.0 - (avgDist / 200));
303
+ return {
304
+ name: 'low_progress',
305
+ score: Math.max(0, score),
306
+ detail: `Avg ${Math.round(avgDist)} chars changed per capture (minor tweaks)`,
307
+ };
308
+ }
309
+
310
+ return { name: 'low_progress', score: 0, detail: `Avg ${Math.round(avgDist)} chars changed (making progress)` };
311
+ }
312
+ }
313
+
314
+ // ── Helpers ──
315
+
316
+ /**
317
+ * Simple non-cryptographic hash for fast comparison.
318
+ */
319
+ function simpleHash(str: string): string {
320
+ let hash = 0;
321
+ const sample = str.slice(0, 2000);
322
+ for (let i = 0; i < sample.length; i++) {
323
+ const char = sample.charCodeAt(i);
324
+ hash = ((hash << 5) - hash) + char;
325
+ hash |= 0;
326
+ }
327
+ return hash.toString(36);
328
+ }
329
+
330
+ /**
331
+ * Cheap O(n) edit distance — counts character-position mismatches.
332
+ * Not a full Levenshtein, but sufficient for "are these texts similar?"
333
+ */
334
+ function cheapEditDistance(a: string, b: string): number {
335
+ const maxLen = Math.min(a.length, b.length, 1000);
336
+ let diffs = 0;
337
+ for (let i = 0; i < maxLen; i++) {
338
+ if (a.charCodeAt(i) !== b.charCodeAt(i)) diffs++;
339
+ }
340
+ diffs += Math.abs(a.length - b.length);
341
+ return diffs;
342
+ }