@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,329 @@
1
+ /**
2
+ * Tests for M16 Phase 7 — Goal System Integrations
3
+ *
4
+ * Tests awareness-bridge (fuzzy matching), workflow-bridge (rhythm workflows),
5
+ * and goal context injection.
6
+ */
7
+
8
+ import { test, expect, beforeEach } from 'bun:test';
9
+ import { initDatabase } from '../vault/schema.ts';
10
+ import { createGoal, updateGoalStatus } from '../vault/goals.ts';
11
+ import { matchAwarenessToGoals, logAutoDetectedProgress } from './awareness-bridge.ts';
12
+ import { generateRhythmWorkflows, registerGoalWorkflows } from './workflow-bridge.ts';
13
+ import { extractGoalCompletion } from '../vault/extractor.ts';
14
+ import { getActiveGoalsSummary } from '../vault/retrieval.ts';
15
+ import { findEntities } from '../vault/entities.ts';
16
+ import { findFacts } from '../vault/facts.ts';
17
+ import type { GoalConfig } from '../config/types.ts';
18
+
19
+ beforeEach(() => {
20
+ initDatabase(':memory:');
21
+ });
22
+
23
+ // ── Awareness Bridge Tests ──────────────────────────────────────────
24
+
25
+ test('matchAwarenessToGoals returns empty when no active goals', () => {
26
+ const matches = matchAwarenessToGoals({
27
+ app_name: 'VS Code',
28
+ window_title: 'index.ts - project',
29
+ });
30
+ expect(matches).toEqual([]);
31
+ });
32
+
33
+ test('matchAwarenessToGoals finds matches for active goals', () => {
34
+ createGoal('Learn TypeScript fundamentals', 'task', {
35
+ description: 'Complete TypeScript tutorial covering generics, interfaces, and type guards',
36
+ status: 'active',
37
+ });
38
+ createGoal('Exercise daily', 'daily_action', {
39
+ description: 'Run or gym workout every day',
40
+ status: 'active',
41
+ });
42
+
43
+ // Event related to TypeScript
44
+ const matches = matchAwarenessToGoals({
45
+ app_name: 'VS Code',
46
+ window_title: 'TypeScript Tutorial - generics.ts',
47
+ ocr_text: 'interface UserProfile extends BaseInterface',
48
+ });
49
+
50
+ expect(matches.length).toBeGreaterThanOrEqual(1);
51
+ expect(matches[0]!.goalTitle).toBe('Learn TypeScript fundamentals');
52
+ expect(matches[0]!.matchedTerms.length).toBeGreaterThanOrEqual(2);
53
+ });
54
+
55
+ test('matchAwarenessToGoals does not match unrelated events', () => {
56
+ createGoal('Learn Python machine learning', 'task', {
57
+ description: 'Complete scikit-learn and tensorflow courses',
58
+ status: 'active',
59
+ });
60
+
61
+ // Unrelated event
62
+ const matches = matchAwarenessToGoals({
63
+ app_name: 'Spotify',
64
+ window_title: 'Playing: Jazz Classics',
65
+ });
66
+
67
+ expect(matches).toEqual([]);
68
+ });
69
+
70
+ test('matchAwarenessToGoals ignores non-active goals', () => {
71
+ createGoal('Learn TypeScript fundamentals', 'task', {
72
+ description: 'Complete TypeScript tutorial covering generics and interfaces',
73
+ status: 'completed', // not active
74
+ });
75
+
76
+ const matches = matchAwarenessToGoals({
77
+ app_name: 'VS Code',
78
+ window_title: 'TypeScript generics tutorial',
79
+ });
80
+
81
+ expect(matches).toEqual([]);
82
+ });
83
+
84
+ test('matchAwarenessToGoals handles session_ended data', () => {
85
+ createGoal('Build web application with React', 'milestone', {
86
+ description: 'Develop a React web application with components and hooks',
87
+ status: 'active',
88
+ });
89
+
90
+ const matches = matchAwarenessToGoals({
91
+ dominant_app: 'VS Code',
92
+ summary: 'Worked on React application components and hooks',
93
+ activities: ['Edited React component files', 'Debugged hooks issue'],
94
+ });
95
+
96
+ expect(matches.length).toBeGreaterThanOrEqual(1);
97
+ expect(matches[0]!.matchedTerms.length).toBeGreaterThanOrEqual(2);
98
+ });
99
+
100
+ test('logAutoDetectedProgress creates progress entries', () => {
101
+ const goal = createGoal('Learn TypeScript', 'task', {
102
+ description: 'Complete TypeScript tutorial',
103
+ status: 'active',
104
+ });
105
+
106
+ const matches = [{
107
+ goalId: goal.id,
108
+ goalTitle: goal.title,
109
+ matchScore: 0.5,
110
+ matchedTerms: ['typescript', 'tutorial'],
111
+ source: 'VS Code',
112
+ }];
113
+
114
+ logAutoDetectedProgress(matches, 'context_changed');
115
+
116
+ // Check progress was logged
117
+ const { getProgressHistory } = require('../vault/goals.ts');
118
+ const progress = getProgressHistory(goal.id, 10);
119
+ expect(progress.length).toBe(1);
120
+ expect(progress[0].type).toBe('auto_detected');
121
+ expect(progress[0].source).toBe('awareness');
122
+ expect(progress[0].note).toContain('typescript');
123
+ });
124
+
125
+ test('logAutoDetectedProgress deduplicates within 30 minutes', () => {
126
+ const goal = createGoal('Learn TypeScript', 'task', {
127
+ description: 'Complete TypeScript tutorial',
128
+ status: 'active',
129
+ });
130
+
131
+ const matches = [{
132
+ goalId: goal.id,
133
+ goalTitle: goal.title,
134
+ matchScore: 0.5,
135
+ matchedTerms: ['typescript', 'tutorial'],
136
+ source: 'VS Code',
137
+ }];
138
+
139
+ // First call logs progress
140
+ logAutoDetectedProgress(matches, 'context_changed');
141
+
142
+ // Second call within 30min should not log again
143
+ logAutoDetectedProgress(matches, 'context_changed');
144
+
145
+ const { getProgressHistory } = require('../vault/goals.ts');
146
+ const progress = getProgressHistory(goal.id, 10);
147
+ expect(progress.length).toBe(1); // still just 1
148
+ });
149
+
150
+ // ── Workflow Bridge Tests ───────────────────────────────────────────
151
+
152
+ test('generateRhythmWorkflows creates morning and evening workflows', () => {
153
+ const config: GoalConfig = {
154
+ enabled: true,
155
+ morning_window: { start: 7, end: 9 },
156
+ evening_window: { start: 20, end: 22 },
157
+ accountability_style: 'drill_sergeant',
158
+ escalation_weeks: { pressure: 1, root_cause: 3, suggest_kill: 4 },
159
+ auto_decompose: true,
160
+ calendar_ownership: false,
161
+ };
162
+
163
+ const workflows = generateRhythmWorkflows(config);
164
+ expect(workflows.length).toBe(2);
165
+
166
+ const morning = workflows.find(w => w.action === 'morning_plan');
167
+ expect(morning).toBeDefined();
168
+ expect(morning!.cronExpression).toBe('0 7 * * *');
169
+ expect(morning!.triggerType).toBe('cron');
170
+
171
+ const evening = workflows.find(w => w.action === 'evening_review');
172
+ expect(evening).toBeDefined();
173
+ expect(evening!.cronExpression).toBe('0 20 * * *');
174
+ });
175
+
176
+ test('generateRhythmWorkflows returns empty when disabled', () => {
177
+ const config: GoalConfig = {
178
+ enabled: false,
179
+ morning_window: { start: 7, end: 9 },
180
+ evening_window: { start: 20, end: 22 },
181
+ accountability_style: 'drill_sergeant',
182
+ escalation_weeks: { pressure: 1, root_cause: 3, suggest_kill: 4 },
183
+ auto_decompose: true,
184
+ calendar_ownership: false,
185
+ };
186
+
187
+ const workflows = generateRhythmWorkflows(config);
188
+ expect(workflows.length).toBe(0);
189
+ });
190
+
191
+ test('generateRhythmWorkflows uses custom window times', () => {
192
+ const config: GoalConfig = {
193
+ enabled: true,
194
+ morning_window: { start: 6, end: 8 },
195
+ evening_window: { start: 21, end: 23 },
196
+ accountability_style: 'supportive',
197
+ escalation_weeks: { pressure: 2, root_cause: 4, suggest_kill: 6 },
198
+ auto_decompose: true,
199
+ calendar_ownership: true,
200
+ };
201
+
202
+ const workflows = generateRhythmWorkflows(config);
203
+ const morning = workflows.find(w => w.action === 'morning_plan')!;
204
+ const evening = workflows.find(w => w.action === 'evening_review')!;
205
+
206
+ expect(morning.cronExpression).toBe('0 6 * * *');
207
+ expect(evening.cronExpression).toBe('0 21 * * *');
208
+ });
209
+
210
+ test('registerGoalWorkflows logs workflows without error', () => {
211
+ const workflows = generateRhythmWorkflows({
212
+ enabled: true,
213
+ morning_window: { start: 7, end: 9 },
214
+ evening_window: { start: 20, end: 22 },
215
+ accountability_style: 'drill_sergeant',
216
+ escalation_weeks: { pressure: 1, root_cause: 3, suggest_kill: 4 },
217
+ auto_decompose: true,
218
+ calendar_ownership: false,
219
+ });
220
+
221
+ const mockTriggerManager = {
222
+ fireTrigger: () => {},
223
+ };
224
+
225
+ // Should not throw
226
+ expect(() => registerGoalWorkflows(workflows, mockTriggerManager)).not.toThrow();
227
+ });
228
+
229
+ // ── Phase 8: Goal Memory + Knowledge ───────────────────────────────
230
+
231
+ test('extractGoalCompletion creates entity with facts', () => {
232
+ const goal = createGoal('Build REST API', 'milestone', {
233
+ description: 'Create a full REST API with CRUD endpoints',
234
+ status: 'active',
235
+ });
236
+
237
+ // Simulate completion
238
+ const completedGoal = {
239
+ ...goal,
240
+ status: 'completed',
241
+ score: 0.85,
242
+ estimated_hours: 20,
243
+ actual_hours: 25.5,
244
+ completed_at: Date.now(),
245
+ tags: ['backend', 'api'],
246
+ };
247
+
248
+ extractGoalCompletion(completedGoal);
249
+
250
+ // Verify entity was created
251
+ const entities = findEntities({ name: 'Build REST API', type: 'concept' });
252
+ expect(entities.length).toBe(1);
253
+ expect(entities[0]!.source).toBe('goal_completion');
254
+
255
+ // Verify facts were stored
256
+ const facts = findFacts({ subject_id: entities[0]!.id });
257
+ const factMap = new Map(facts.map(f => [f.predicate, f.object]));
258
+
259
+ expect(factMap.get('goal_final_score')).toBe('0.85');
260
+ expect(factMap.get('goal_outcome')).toBe('completed');
261
+ expect(factMap.get('goal_level')).toBe('milestone');
262
+ expect(factMap.get('estimated_hours')).toBe('20');
263
+ expect(factMap.get('actual_hours')).toBe('25.5');
264
+ expect(factMap.get('goal_tags')).toBe('backend, api');
265
+ expect(factMap.has('days_to_complete')).toBe(true);
266
+ expect(factMap.has('estimation_accuracy')).toBe(true);
267
+ });
268
+
269
+ test('extractGoalCompletion handles failed goals', () => {
270
+ extractGoalCompletion({
271
+ id: 'test-failed',
272
+ title: 'Learn Rust',
273
+ level: 'objective',
274
+ score: 0.2,
275
+ status: 'failed',
276
+ estimated_hours: null,
277
+ actual_hours: 0,
278
+ created_at: Date.now() - 86400000 * 30,
279
+ completed_at: null,
280
+ tags: [],
281
+ });
282
+
283
+ const entities = findEntities({ name: 'Learn Rust', type: 'concept' });
284
+ expect(entities.length).toBe(1);
285
+
286
+ const facts = findFacts({ subject_id: entities[0]!.id });
287
+ const factMap = new Map(facts.map(f => [f.predicate, f.object]));
288
+ expect(factMap.get('goal_outcome')).toBe('failed');
289
+ expect(factMap.get('goal_final_score')).toBe('0.20');
290
+ // No estimated_hours or actual_hours since they're null/0
291
+ expect(factMap.has('estimated_hours')).toBe(false);
292
+ expect(factMap.has('actual_hours')).toBe(false);
293
+ });
294
+
295
+ test('getActiveGoalsSummary returns formatted goal list', () => {
296
+ createGoal('Get fit', 'objective', { status: 'active' });
297
+ createGoal('Run daily', 'key_result', { status: 'active' });
298
+ createGoal('Complete project', 'milestone', { status: 'active' });
299
+
300
+ const summary = getActiveGoalsSummary();
301
+ expect(summary).toContain('Get fit');
302
+ expect(summary).toContain('Run daily');
303
+ expect(summary).toContain('Complete project');
304
+ expect(summary).toContain('/1.0');
305
+ });
306
+
307
+ test('getActiveGoalsSummary returns empty when no active goals', () => {
308
+ createGoal('Done goal', 'task', { status: 'completed' });
309
+
310
+ const summary = getActiveGoalsSummary();
311
+ expect(summary).toBe('');
312
+ });
313
+
314
+ test('getActiveGoalsSummary sorts by level hierarchy', () => {
315
+ createGoal('Daily run', 'daily_action', { status: 'active' });
316
+ createGoal('Get fit', 'objective', { status: 'active' });
317
+ createGoal('Lose 10 lbs', 'key_result', { status: 'active' });
318
+
319
+ const summary = getActiveGoalsSummary();
320
+ const lines = summary.split('\n');
321
+
322
+ // Objective should come before key_result, which should come before daily_action
323
+ const fitIndex = lines.findIndex(l => l.includes('Get fit'));
324
+ const loseIndex = lines.findIndex(l => l.includes('Lose 10 lbs'));
325
+ const runIndex = lines.findIndex(l => l.includes('Daily run'));
326
+
327
+ expect(fitIndex).toBeLessThan(loseIndex);
328
+ expect(loseIndex).toBeLessThan(runIndex);
329
+ });
@@ -0,0 +1,220 @@
1
+ import { test, expect, describe, beforeEach } from 'bun:test';
2
+ import { initDatabase } from '../vault/schema.ts';
3
+ import { NLGoalBuilder, type GoalProposal } from './nl-builder.ts';
4
+ import { GoalEstimator } from './estimator.ts';
5
+ import * as vault from '../vault/goals.ts';
6
+
7
+ // Mock LLM manager — returns canned responses
8
+ const mockLLM = {
9
+ chat: async (messages: any[], _opts?: any) => {
10
+ const lastMsg = messages[messages.length - 1];
11
+ const content = typeof lastMsg.content === 'string' ? lastMsg.content : '';
12
+
13
+ // Default proposal response
14
+ if (content.includes('Convert this into an OKR') || content.includes('Decompose this')) {
15
+ return {
16
+ content: JSON.stringify({
17
+ objective: {
18
+ title: 'Ship MVP',
19
+ description: 'Launch the minimum viable product',
20
+ success_criteria: '100 active users within 30 days of launch',
21
+ time_horizon: 'quarterly',
22
+ deadline_days: 90,
23
+ tags: ['product', 'launch'],
24
+ },
25
+ key_results: [
26
+ {
27
+ title: 'Complete core features',
28
+ description: 'Build all must-have features',
29
+ success_criteria: '5 core features deployed and tested',
30
+ deadline_days: 60,
31
+ },
32
+ {
33
+ title: 'Launch marketing site',
34
+ description: 'Build and deploy landing page',
35
+ success_criteria: 'Site live with signup form',
36
+ deadline_days: 45,
37
+ },
38
+ ],
39
+ milestones: [
40
+ {
41
+ key_result_index: 0,
42
+ title: 'Auth system complete',
43
+ description: 'User registration and login working',
44
+ deadline_days: 30,
45
+ },
46
+ ],
47
+ }),
48
+ };
49
+ }
50
+
51
+ // Estimation response
52
+ if (content.includes('Estimate hours')) {
53
+ return {
54
+ content: JSON.stringify({
55
+ hours: 40,
56
+ confidence: 0.6,
57
+ reasoning: 'Medium complexity project with standard web stack',
58
+ }),
59
+ };
60
+ }
61
+
62
+ // Chat response
63
+ return { content: 'Here are some suggestions for improving your goal.' };
64
+ },
65
+ };
66
+
67
+ describe('NLGoalBuilder', () => {
68
+ let builder: NLGoalBuilder;
69
+
70
+ beforeEach(() => {
71
+ initDatabase(':memory:');
72
+ builder = new NLGoalBuilder(mockLLM);
73
+ });
74
+
75
+ test('parseGoal returns structured proposal', async () => {
76
+ const proposal = await builder.parseGoal('I want to ship my MVP this quarter');
77
+
78
+ expect(proposal.objective.title).toBe('Ship MVP');
79
+ expect(proposal.key_results.length).toBe(2);
80
+ expect(proposal.milestones?.length).toBe(1);
81
+ expect(proposal.objective.tags).toEqual(['product', 'launch']);
82
+ });
83
+
84
+ test('createFromProposal creates goal hierarchy', async () => {
85
+ const proposal = await builder.parseGoal('Ship MVP');
86
+ const goals = builder.createFromProposal(proposal);
87
+
88
+ expect(goals.length).toBe(4); // 1 objective + 2 key results + 1 milestone
89
+
90
+ // Goals created in order: objective, KR1, milestone (under KR1), KR2
91
+ expect(goals[0]!.level).toBe('objective');
92
+ expect(goals[0]!.title).toBe('Ship MVP');
93
+ expect(goals[1]!.level).toBe('key_result');
94
+ expect(goals[1]!.parent_id).toBe(goals[0]!.id);
95
+ expect(goals[2]!.level).toBe('milestone');
96
+ expect(goals[2]!.parent_id).toBe(goals[1]!.id); // Under first KR
97
+ expect(goals[3]!.level).toBe('key_result');
98
+ expect(goals[3]!.parent_id).toBe(goals[0]!.id);
99
+ });
100
+
101
+ test('createFromProposal with parent_id', async () => {
102
+ const parent = vault.createGoal('Parent Objective', 'objective');
103
+ const proposal = await builder.parseGoal('Sub-objective');
104
+ const goals = builder.createFromProposal(proposal, parent.id);
105
+
106
+ expect(goals[0]!.parent_id).toBe(parent.id);
107
+ });
108
+
109
+ test('decompose returns proposal for children', async () => {
110
+ const goal = vault.createGoal('Root Objective', 'objective', {
111
+ description: 'A big goal',
112
+ success_criteria: 'It works',
113
+ });
114
+
115
+ const proposal = await builder.decompose(goal.id);
116
+ expect(proposal).not.toBeNull();
117
+ expect(proposal!.objective.title).toBe('Ship MVP');
118
+ });
119
+
120
+ test('decompose returns null for non-existent goal', async () => {
121
+ const proposal = await builder.decompose('nonexistent');
122
+ expect(proposal).toBeNull();
123
+ });
124
+
125
+ test('decompose returns null for daily_action (no next level)', async () => {
126
+ const goal = vault.createGoal('Daily task', 'daily_action');
127
+ const proposal = await builder.decompose(goal.id);
128
+ expect(proposal).toBeNull();
129
+ });
130
+
131
+ test('chat returns text reply', async () => {
132
+ const goal = vault.createGoal('Chat goal', 'objective');
133
+ const result = await builder.chat(goal.id, 'How can I improve this?', []);
134
+
135
+ expect(result.reply).toBeTruthy();
136
+ expect(result.proposal).toBeUndefined();
137
+ });
138
+ });
139
+
140
+ describe('GoalEstimator', () => {
141
+ let estimator: GoalEstimator;
142
+
143
+ beforeEach(() => {
144
+ initDatabase(':memory:');
145
+ estimator = new GoalEstimator(mockLLM);
146
+ });
147
+
148
+ test('quickEstimate returns heuristic for no history', () => {
149
+ const est = estimator.quickEstimate('Build auth', 'task');
150
+ expect(est.hours).toBe(4);
151
+ expect(est.confidence).toBe(0.2);
152
+ });
153
+
154
+ test('quickEstimate uses history when available', () => {
155
+ // Create some completed goals with actual hours
156
+ const g1 = vault.createGoal('Build login', 'task', { status: 'active' });
157
+ vault.updateGoalActualHours(g1.id, 6);
158
+ vault.updateGoalStatus(g1.id, 'completed');
159
+
160
+ const g2 = vault.createGoal('Build signup', 'task', { status: 'active' });
161
+ vault.updateGoalActualHours(g2.id, 8);
162
+ vault.updateGoalStatus(g2.id, 'completed');
163
+
164
+ const est = estimator.quickEstimate('Build auth flow', 'task');
165
+ // Should find similar goals and use their average
166
+ expect(est.hours).toBe(7); // avg of 6 and 8
167
+ expect(est.confidence).toBe(0.6);
168
+ });
169
+
170
+ test('estimate returns null for non-existent goal', async () => {
171
+ const est = await estimator.estimate('nonexistent');
172
+ expect(est).toBeNull();
173
+ });
174
+
175
+ test('estimate returns LLM-based estimate when no history', async () => {
176
+ const goal = vault.createGoal('New feature', 'key_result', {
177
+ description: 'Build something new',
178
+ success_criteria: 'It works well',
179
+ });
180
+
181
+ const est = await estimator.estimate(goal.id);
182
+ expect(est).not.toBeNull();
183
+ expect(est!.llm_estimate_hours).toBe(40);
184
+ expect(est!.historical_estimate_hours).toBeNull();
185
+ expect(est!.final_estimate_hours).toBe(40);
186
+ expect(est!.reasoning).toContain('No historical data');
187
+ });
188
+
189
+ test('estimate blends history and LLM', async () => {
190
+ // Create completed goals with matching words
191
+ const g1 = vault.createGoal('Build feature alpha', 'key_result', {
192
+ status: 'active',
193
+ description: 'Build something',
194
+ });
195
+ vault.updateGoalActualHours(g1.id, 30);
196
+ vault.updateGoalStatus(g1.id, 'completed');
197
+
198
+ const g2 = vault.createGoal('Build feature beta', 'key_result', {
199
+ status: 'active',
200
+ description: 'Build another thing',
201
+ });
202
+ vault.updateGoalActualHours(g2.id, 50);
203
+ vault.updateGoalStatus(g2.id, 'completed');
204
+
205
+ // Create goal to estimate
206
+ const goal = vault.createGoal('Build feature gamma', 'key_result', {
207
+ description: 'Build yet another thing',
208
+ success_criteria: 'Feature complete',
209
+ });
210
+
211
+ const est = await estimator.estimate(goal.id);
212
+ expect(est).not.toBeNull();
213
+ expect(est!.historical_estimate_hours).toBe(40); // avg of 30 and 50
214
+ expect(est!.llm_estimate_hours).toBe(40);
215
+ // 60% history (40) + 40% LLM (40) = 40
216
+ expect(est!.final_estimate_hours).toBe(40);
217
+ expect(est!.confidence).toBeGreaterThan(0.5);
218
+ expect(est!.similar_past_goals.length).toBe(2);
219
+ });
220
+ });