@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,256 @@
1
+ /**
2
+ * NL Goal Builder — Natural language to OKR decomposition
3
+ *
4
+ * Converts freeform user descriptions into structured goal hierarchies.
5
+ * Supports iterative refinement via chat, and full decomposition to daily actions.
6
+ */
7
+
8
+ import type { Goal, GoalLevel } from './types.ts';
9
+ import * as vault from '../vault/goals.ts';
10
+
11
+ type ChatMessage = { role: 'user' | 'assistant'; content: string };
12
+
13
+ export type GoalProposal = {
14
+ objective: {
15
+ title: string;
16
+ description: string;
17
+ success_criteria: string;
18
+ time_horizon: string;
19
+ deadline_days?: number;
20
+ tags?: string[];
21
+ };
22
+ key_results: {
23
+ title: string;
24
+ description: string;
25
+ success_criteria: string;
26
+ deadline_days?: number;
27
+ }[];
28
+ milestones?: {
29
+ key_result_index: number;
30
+ title: string;
31
+ description: string;
32
+ deadline_days?: number;
33
+ }[];
34
+ clarifying_questions?: string[];
35
+ };
36
+
37
+ export class NLGoalBuilder {
38
+ private llmManager: any; // LLMManager
39
+
40
+ constructor(llmManager: unknown) {
41
+ this.llmManager = llmManager;
42
+ }
43
+
44
+ /**
45
+ * Parse a natural language goal description into a structured OKR proposal.
46
+ */
47
+ async parseGoal(text: string): Promise<GoalProposal> {
48
+ const existingGoals = vault.getRootGoals().slice(0, 10);
49
+ const existingContext = existingGoals.length > 0
50
+ ? `\n\nExisting goals for context (avoid duplicates):\n${existingGoals.map(g => `- ${g.title} (${g.level}, ${g.status})`).join('\n')}`
51
+ : '';
52
+
53
+ const prompt = [
54
+ { role: 'system' as const, content: this.buildSystemPrompt() },
55
+ {
56
+ role: 'user' as const,
57
+ content: `Convert this into an OKR goal hierarchy:\n\n"${text}"${existingContext}\n\nRespond with ONLY valid JSON matching the GoalProposal schema. No explanation.`,
58
+ },
59
+ ];
60
+
61
+ const response = await this.llmManager.chat(prompt, {
62
+ temperature: 0.3,
63
+ max_tokens: 4000,
64
+ });
65
+
66
+ return this.parseResponse(response.content);
67
+ }
68
+
69
+ /**
70
+ * Decompose an existing goal into child goals at the next level.
71
+ */
72
+ async decompose(goalId: string, depth: GoalLevel = 'task'): Promise<GoalProposal | null> {
73
+ const goal = vault.getGoal(goalId);
74
+ if (!goal) return null;
75
+
76
+ const children = vault.getGoalChildren(goalId);
77
+ const childContext = children.length > 0
78
+ ? `\nExisting children:\n${children.map(c => `- ${c.title} (${c.level})`).join('\n')}`
79
+ : '';
80
+
81
+ const nextLevel = this.getNextLevel(goal.level);
82
+ if (!nextLevel) return null;
83
+
84
+ const prompt = [
85
+ { role: 'system' as const, content: this.buildSystemPrompt() },
86
+ {
87
+ role: 'user' as const,
88
+ content: `Decompose this ${goal.level} into ${nextLevel}s:\n\nTitle: ${goal.title}\nDescription: ${goal.description}\nSuccess criteria: ${goal.success_criteria}\nTime horizon: ${goal.time_horizon}${childContext}\n\nTarget depth: ${depth}\n\nRespond with ONLY valid JSON matching the GoalProposal schema (use key_results array for the sub-goals regardless of level). No explanation.`,
89
+ },
90
+ ];
91
+
92
+ const response = await this.llmManager.chat(prompt, {
93
+ temperature: 0.3,
94
+ max_tokens: 4000,
95
+ });
96
+
97
+ return this.parseResponse(response.content);
98
+ }
99
+
100
+ /**
101
+ * Conversational goal refinement — chat with history to iteratively build goals.
102
+ */
103
+ async chat(
104
+ goalId: string,
105
+ message: string,
106
+ history: ChatMessage[],
107
+ ): Promise<{ reply: string; proposal?: GoalProposal }> {
108
+ const goal = vault.getGoal(goalId);
109
+ const tree = goal ? vault.getGoalTree(goalId) : [];
110
+
111
+ const treeContext = tree.length > 0
112
+ ? `\nCurrent goal tree:\n${tree.map(g => `${' '.repeat(this.levelDepth(g.level))}${g.title} (${g.level}, score: ${g.score}, status: ${g.status})`).join('\n')}`
113
+ : '';
114
+
115
+ const messages = [
116
+ { role: 'system' as const, content: this.buildChatPrompt(treeContext) },
117
+ ...history.map(h => ({ role: h.role as 'user' | 'assistant', content: h.content })),
118
+ { role: 'user' as const, content: message },
119
+ ];
120
+
121
+ const response = await this.llmManager.chat(messages, {
122
+ temperature: 0.4,
123
+ max_tokens: 3000,
124
+ });
125
+
126
+ const content = typeof response.content === 'string' ? response.content : '';
127
+
128
+ // Check if response contains a JSON proposal
129
+ const jsonMatch = content.match(/```json\n([\s\S]*?)\n```/);
130
+ if (jsonMatch) {
131
+ try {
132
+ const proposal = JSON.parse(jsonMatch[1]) as GoalProposal;
133
+ const textBefore = content.slice(0, content.indexOf('```json')).trim();
134
+ return { reply: textBefore || 'Here is the updated proposal:', proposal };
135
+ } catch { /* not valid JSON, treat as text */ }
136
+ }
137
+
138
+ return { reply: content };
139
+ }
140
+
141
+ /**
142
+ * Create goal hierarchy from a confirmed proposal.
143
+ */
144
+ createFromProposal(proposal: GoalProposal, parentId?: string): Goal[] {
145
+ const created: Goal[] = [];
146
+
147
+ const objective = vault.createGoal(proposal.objective.title, 'objective', {
148
+ parent_id: parentId,
149
+ description: proposal.objective.description,
150
+ success_criteria: proposal.objective.success_criteria,
151
+ time_horizon: proposal.objective.time_horizon as any,
152
+ deadline: proposal.objective.deadline_days
153
+ ? Date.now() + proposal.objective.deadline_days * 86400000
154
+ : undefined,
155
+ tags: proposal.objective.tags,
156
+ });
157
+ created.push(objective);
158
+
159
+ for (let i = 0; i < proposal.key_results.length; i++) {
160
+ const kr = proposal.key_results[i]!;
161
+ const keyResult = vault.createGoal(kr.title, 'key_result', {
162
+ parent_id: objective.id,
163
+ description: kr.description,
164
+ success_criteria: kr.success_criteria,
165
+ deadline: kr.deadline_days
166
+ ? Date.now() + kr.deadline_days * 86400000
167
+ : undefined,
168
+ });
169
+ created.push(keyResult);
170
+
171
+ // Add milestones under their key results
172
+ if (proposal.milestones) {
173
+ for (const ms of proposal.milestones) {
174
+ if (ms.key_result_index === i) {
175
+ const milestone = vault.createGoal(ms.title, 'milestone', {
176
+ parent_id: keyResult.id,
177
+ description: ms.description,
178
+ deadline: ms.deadline_days
179
+ ? Date.now() + ms.deadline_days * 86400000
180
+ : undefined,
181
+ });
182
+ created.push(milestone);
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ return created;
189
+ }
190
+
191
+ // ── Helpers ──────────────────────────────────────────────────────
192
+
193
+ private buildSystemPrompt(): string {
194
+ return `You are an OKR (Objectives and Key Results) expert using Google-style scoring (0.0-1.0 scale, where 0.7 = good, 1.0 = aimed too low).
195
+
196
+ Rules:
197
+ - Objectives are qualitative, ambitious, and inspiring
198
+ - Key Results are specific, measurable, and time-bound
199
+ - Milestones break Key Results into concrete deliverables
200
+ - Tasks are actionable work items
201
+ - Daily Actions are single-day activities
202
+ - Goal hierarchy: objective → key_result → milestone → task → daily_action
203
+ - Time horizons: life, yearly, quarterly, monthly, weekly, daily
204
+ - Be specific with success criteria — use numbers, dates, concrete outcomes
205
+ - Create 2-5 Key Results per Objective
206
+ - Create 1-3 Milestones per Key Result when appropriate
207
+
208
+ Respond with ONLY valid JSON matching this schema:
209
+ {
210
+ "objective": { "title": string, "description": string, "success_criteria": string, "time_horizon": string, "deadline_days?": number, "tags?": string[] },
211
+ "key_results": [{ "title": string, "description": string, "success_criteria": string, "deadline_days?": number }],
212
+ "milestones?": [{ "key_result_index": number, "title": string, "description": string, "deadline_days?": number }],
213
+ "clarifying_questions?": string[]
214
+ }`;
215
+ }
216
+
217
+ private buildChatPrompt(treeContext: string): string {
218
+ return `You are an OKR coach helping refine goals. Be direct and constructive.${treeContext}
219
+
220
+ When the user wants to change goals, include a JSON proposal in a \`\`\`json code block. Otherwise, respond conversationally with advice and questions.`;
221
+ }
222
+
223
+ private parseResponse(content: string | unknown): GoalProposal {
224
+ const text = typeof content === 'string' ? content : JSON.stringify(content);
225
+ const json = this.extractJson(text);
226
+ return JSON.parse(json) as GoalProposal;
227
+ }
228
+
229
+ private extractJson(text: string): string {
230
+ // Try code block first
231
+ const codeBlock = text.match(/```(?:json)?\n?([\s\S]*?)\n?```/);
232
+ if (codeBlock) return codeBlock[1]!;
233
+
234
+ // Try raw JSON
235
+ const jsonStart = text.indexOf('{');
236
+ const jsonEnd = text.lastIndexOf('}');
237
+ if (jsonStart !== -1 && jsonEnd > jsonStart) {
238
+ return text.slice(jsonStart, jsonEnd + 1);
239
+ }
240
+
241
+ return text;
242
+ }
243
+
244
+ private getNextLevel(level: GoalLevel): GoalLevel | null {
245
+ const order: GoalLevel[] = ['objective', 'key_result', 'milestone', 'task', 'daily_action'];
246
+ const idx = order.indexOf(level);
247
+ return idx < order.length - 1 ? order[idx + 1]! : null;
248
+ }
249
+
250
+ private levelDepth(level: GoalLevel): number {
251
+ const depths: Record<GoalLevel, number> = {
252
+ objective: 0, key_result: 1, milestone: 2, task: 3, daily_action: 4,
253
+ };
254
+ return depths[level];
255
+ }
256
+ }
@@ -0,0 +1,177 @@
1
+ import { test, expect, describe, beforeEach } from 'bun:test';
2
+ import { initDatabase } from '../vault/schema.ts';
3
+ import { DailyRhythm } from './rhythm.ts';
4
+ import { AccountabilityEngine } from './accountability.ts';
5
+ import * as vault from '../vault/goals.ts';
6
+ import type { GoalEvent } from './events.ts';
7
+
8
+ // Mock LLM manager
9
+ const mockLLM = {
10
+ chat: async (messages: any[], _opts?: any) => {
11
+ const lastMsg = messages[messages.length - 1];
12
+ const content = typeof lastMsg.content === 'string' ? lastMsg.content : '';
13
+
14
+ if (content.includes('morning plan')) {
15
+ return {
16
+ content: JSON.stringify({
17
+ focus_areas: ['Ship feature X', 'Review PR'],
18
+ daily_actions: ['Write tests for feature X', 'Code review PR #42'],
19
+ warnings: ['Deadline approaching for KR-1'],
20
+ message: 'Stop procrastinating. Ship it.',
21
+ }),
22
+ };
23
+ }
24
+
25
+ if (content.includes('Review the day')) {
26
+ return {
27
+ content: JSON.stringify({
28
+ score_updates: [],
29
+ actions_completed: ['Wrote tests for feature X'],
30
+ assessment: 'Decent day. Could have done more.',
31
+ message: 'Not bad. Not great. Do better tomorrow.',
32
+ }),
33
+ };
34
+ }
35
+
36
+ if (content.includes('PRESSURE') || content.includes('ROOT CAUSE') || content.includes('KILL')) {
37
+ return { content: 'You are behind. Fix it now.' };
38
+ }
39
+
40
+ if (content.includes('replan')) {
41
+ return {
42
+ content: JSON.stringify({
43
+ options: [
44
+ { id: 'extend', label: 'Extend deadline', description: 'Add 2 weeks', impact: 'low' },
45
+ { id: 'kill', label: 'Kill it', description: 'Move on', impact: 'high' },
46
+ ],
47
+ analysis: 'The goal is too ambitious for the timeline.',
48
+ recommendation: 'Extend the deadline.',
49
+ }),
50
+ };
51
+ }
52
+
53
+ return { content: 'OK' };
54
+ },
55
+ };
56
+
57
+ describe('DailyRhythm', () => {
58
+ let rhythm: DailyRhythm;
59
+ let events: GoalEvent[];
60
+
61
+ beforeEach(() => {
62
+ initDatabase(':memory:');
63
+ rhythm = new DailyRhythm(mockLLM, 'drill_sergeant');
64
+ events = [];
65
+ rhythm.setEventCallback((e) => events.push(e));
66
+ });
67
+
68
+ test('runMorningPlan creates check-in', async () => {
69
+ vault.createGoal('Active Goal', 'task', { status: 'active' });
70
+
71
+ const result = await rhythm.runMorningPlan();
72
+
73
+ expect(result.checkIn.type).toBe('morning_plan');
74
+ expect(result.focusAreas.length).toBeGreaterThan(0);
75
+ expect(result.dailyActions.length).toBeGreaterThan(0);
76
+ expect(result.message).toBeTruthy();
77
+
78
+ // Event emitted
79
+ expect(events.length).toBe(1);
80
+ expect(events[0]!.type).toBe('check_in_morning');
81
+
82
+ // Check-in stored in DB
83
+ const todayCheckIn = vault.getTodayCheckIn('morning_plan');
84
+ expect(todayCheckIn).not.toBeNull();
85
+ });
86
+
87
+ test('runEveningReview creates check-in', async () => {
88
+ vault.createGoal('Active Goal', 'task', { status: 'active' });
89
+
90
+ // Create a morning check-in first
91
+ vault.createCheckIn('morning_plan', 'Morning focus', ['g1'], ['Write code']);
92
+
93
+ const result = await rhythm.runEveningReview();
94
+
95
+ expect(result.checkIn.type).toBe('evening_review');
96
+ expect(result.assessment).toBeTruthy();
97
+ expect(result.message).toBeTruthy();
98
+
99
+ // Event emitted
100
+ expect(events.length).toBe(1);
101
+ expect(events[0]!.type).toBe('check_in_evening');
102
+ });
103
+
104
+ test('runMorningPlan works with no active goals', async () => {
105
+ const result = await rhythm.runMorningPlan();
106
+ expect(result.checkIn).toBeTruthy();
107
+ });
108
+
109
+ test('runEveningReview works without morning plan', async () => {
110
+ vault.createGoal('Goal', 'task', { status: 'active' });
111
+ const result = await rhythm.runEveningReview();
112
+ expect(result.checkIn).toBeTruthy();
113
+ });
114
+ });
115
+
116
+ describe('AccountabilityEngine', () => {
117
+ let engine: AccountabilityEngine;
118
+
119
+ beforeEach(() => {
120
+ initDatabase(':memory:');
121
+ engine = new AccountabilityEngine(mockLLM, 'drill_sergeant', {
122
+ pressure: 1,
123
+ root_cause: 3,
124
+ suggest_kill: 4,
125
+ });
126
+ });
127
+
128
+ test('runEscalationCheck returns empty for healthy goals', () => {
129
+ vault.createGoal('Healthy', 'task', { status: 'active' });
130
+ const actions = engine.runEscalationCheck();
131
+ expect(actions.length).toBe(0);
132
+ });
133
+
134
+ test('runEscalationCheck detects goals needing escalation', () => {
135
+ const goal = vault.createGoal('Behind Goal', 'task', { status: 'active' });
136
+ vault.updateGoalHealth(goal.id, 'behind');
137
+ // Simulate being behind for 2 weeks
138
+ const db = (vault as any).__test_getDb?.() ?? require('../vault/schema.ts').getDb();
139
+ const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
140
+ db.prepare('UPDATE goals SET updated_at = ? WHERE id = ?').run(twoWeeksAgo, goal.id);
141
+
142
+ const actions = engine.runEscalationCheck();
143
+ expect(actions.length).toBe(1);
144
+ expect(actions[0]!.newStage).toBe('pressure');
145
+ });
146
+
147
+ test('generateEscalationMessage returns text', async () => {
148
+ const goal = vault.createGoal('Failing Goal', 'task', { status: 'active' });
149
+ vault.updateGoalHealth(goal.id, 'behind');
150
+
151
+ const message = await engine.generateEscalationMessage(
152
+ vault.getGoal(goal.id)!,
153
+ 'pressure',
154
+ );
155
+ expect(message).toBeTruthy();
156
+ expect(message.length).toBeGreaterThan(0);
157
+ });
158
+
159
+ test('generateEscalationMessage returns empty for none stage', async () => {
160
+ const goal = vault.createGoal('OK Goal', 'task', { status: 'active' });
161
+ const message = await engine.generateEscalationMessage(vault.getGoal(goal.id)!, 'none');
162
+ expect(message).toBe('');
163
+ });
164
+
165
+ test('generateReplanOptions returns options', async () => {
166
+ const goal = vault.createGoal('Stuck Goal', 'objective', {
167
+ status: 'active',
168
+ description: 'Big project',
169
+ success_criteria: 'Ship it',
170
+ });
171
+
172
+ const analysis = await engine.generateReplanOptions(vault.getGoal(goal.id)!);
173
+ expect(analysis.options.length).toBeGreaterThanOrEqual(2);
174
+ expect(analysis.analysis).toBeTruthy();
175
+ expect(analysis.recommendation).toBeTruthy();
176
+ });
177
+ });