@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.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- 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
|
+
});
|