@masslessai/push-todo 3.0.0 → 3.2.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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "push-todo",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Voice tasks from Push iOS app"
5
5
  }
Binary file
@@ -26,7 +26,7 @@ function getApiKey() {
26
26
 
27
27
  try {
28
28
  const content = readFileSync(CONFIG_FILE, 'utf8');
29
- const match = content.match(/^export\s+PUSH_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
29
+ const match = content.match(/^export\s+PUSH_API_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
30
30
  return match ? match[1] : null;
31
31
  } catch {
32
32
  return null;
@@ -2,16 +2,67 @@
2
2
  /**
3
3
  * Session start hook for Push CLI.
4
4
  *
5
- * Displays task count notification when Claude Code starts.
5
+ * 1. Ensures CLI is installed (for marketplace installations)
6
+ * 2. Displays task count notification when Claude Code starts
6
7
  */
7
8
 
8
- import { execSync } from 'child_process';
9
+ import { execSync, spawnSync } from 'child_process';
9
10
  import { existsSync, readFileSync } from 'fs';
10
11
  import { homedir } from 'os';
11
12
  import { join } from 'path';
12
13
 
13
14
  const CONFIG_FILE = join(homedir(), '.config', 'push', 'config');
14
15
  const API_BASE = 'https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1';
16
+ const NPM_PACKAGE = '@masslessai/push-todo';
17
+
18
+ /**
19
+ * Check if push-todo CLI is available.
20
+ */
21
+ function isCLIInstalled() {
22
+ try {
23
+ execSync('which push-todo', {
24
+ encoding: 'utf8',
25
+ timeout: 3000,
26
+ stdio: ['pipe', 'pipe', 'pipe']
27
+ });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Install the CLI via npm (for marketplace installations).
36
+ */
37
+ function ensureCLIInstalled() {
38
+ if (isCLIInstalled()) {
39
+ return true;
40
+ }
41
+
42
+ console.log(`[Push] Installing CLI tools...`);
43
+
44
+ try {
45
+ // Use spawnSync to allow npm to find itself
46
+ const result = spawnSync('npm', ['install', '-g', NPM_PACKAGE], {
47
+ encoding: 'utf8',
48
+ timeout: 60000,
49
+ stdio: ['pipe', 'pipe', 'pipe'],
50
+ shell: true
51
+ });
52
+
53
+ if (result.status === 0) {
54
+ console.log(`[Push] CLI installed successfully.`);
55
+ return true;
56
+ } else {
57
+ console.log(`[Push] CLI installation failed. Run manually: npm install -g ${NPM_PACKAGE}`);
58
+ return false;
59
+ }
60
+ } catch (error) {
61
+ console.log(`[Push] CLI installation failed: ${error.message}`);
62
+ console.log(`[Push] Run manually: npm install -g ${NPM_PACKAGE}`);
63
+ return false;
64
+ }
65
+ }
15
66
 
16
67
  /**
17
68
  * Get the API key from config.
@@ -29,7 +80,7 @@ function getApiKey() {
29
80
 
30
81
  try {
31
82
  const content = readFileSync(CONFIG_FILE, 'utf8');
32
- const match = content.match(/^export\s+PUSH_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
83
+ const match = content.match(/^export\s+PUSH_API_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
33
84
  return match ? match[1] : null;
34
85
  } catch {
35
86
  return null;
@@ -108,10 +159,16 @@ async function fetchTaskCount(apiKey, gitRemote) {
108
159
  * Main entry point.
109
160
  */
110
161
  async function main() {
162
+ // Step 1: Ensure CLI is installed (for marketplace installations)
163
+ // This is a no-op if CLI already exists (npm install path)
164
+ ensureCLIInstalled();
165
+
166
+ // Step 2: Check for API key
111
167
  const apiKey = getApiKey();
112
168
 
113
169
  if (!apiKey) {
114
- // No API key - silent exit
170
+ // No API key - prompt to connect
171
+ console.log(`[Push] Run 'push-todo connect' to set up your account.`);
115
172
  process.exit(0);
116
173
  }
117
174
 
package/lib/api.js CHANGED
@@ -118,12 +118,20 @@ export async function fetchTaskByNumber(displayNumber) {
118
118
  * @returns {Promise<boolean>} True if successful
119
119
  */
120
120
  export async function markTaskCompleted(taskId, comment = '') {
121
- const response = await apiRequest('mark-todo-completed', {
122
- method: 'POST',
123
- body: JSON.stringify({
124
- todo_id: taskId,
125
- completion_comment: comment
126
- })
121
+ const payload = {
122
+ todoId: taskId,
123
+ isCompleted: true,
124
+ completedAt: new Date().toISOString()
125
+ };
126
+
127
+ // Add completion comment if provided (appears in Push app timeline)
128
+ if (comment) {
129
+ payload.completionComment = comment;
130
+ }
131
+
132
+ const response = await apiRequest('todo-status', {
133
+ method: 'PATCH',
134
+ body: JSON.stringify(payload)
127
135
  });
128
136
 
129
137
  if (!response.ok) {
@@ -137,14 +145,18 @@ export async function markTaskCompleted(taskId, comment = '') {
137
145
  /**
138
146
  * Queue a task for daemon execution.
139
147
  *
148
+ * Sets execution_status to 'queued' via the update-task-execution endpoint.
149
+ * The daemon will pick it up on next poll.
150
+ *
140
151
  * @param {number} displayNumber - The task's display number
141
152
  * @returns {Promise<boolean>} True if successful
142
153
  */
143
154
  export async function queueTask(displayNumber) {
144
- const response = await apiRequest('queue-task', {
145
- method: 'POST',
155
+ const response = await apiRequest('update-task-execution', {
156
+ method: 'PATCH',
146
157
  body: JSON.stringify({
147
- display_number: displayNumber
158
+ displayNumber: displayNumber,
159
+ status: 'queued'
148
160
  })
149
161
  });
150
162
 
@@ -153,7 +165,8 @@ export async function queueTask(displayNumber) {
153
165
  throw new Error(`Failed to queue task: ${text}`);
154
166
  }
155
167
 
156
- return true;
168
+ const data = await response.json();
169
+ return data.success || false;
157
170
  }
158
171
 
159
172
  /**
@@ -189,7 +202,7 @@ export async function queueTasks(displayNumbers) {
189
202
  */
190
203
  export async function searchTasks(query, gitRemote = null) {
191
204
  const params = new URLSearchParams();
192
- params.set('query', query);
205
+ params.set('q', query); // Edge function expects 'q', not 'query'
193
206
  if (gitRemote) {
194
207
  params.set('git_remote', gitRemote);
195
208
  }
@@ -231,11 +244,16 @@ export async function updateTaskExecution(payload) {
231
244
  /**
232
245
  * Validate API key.
233
246
  *
247
+ * Uses synced-todos with limit=0 as a lightweight validation check.
248
+ * This matches the Python implementation approach.
249
+ *
234
250
  * @returns {Promise<Object>} Validation result with user info
235
251
  */
236
252
  export async function validateApiKey() {
237
253
  try {
238
- const response = await apiRequest('validate-api-key');
254
+ // Use synced-todos with limit=0 as lightweight validation
255
+ // (no validate-api-key endpoint exists)
256
+ const response = await apiRequest('synced-todos?limit=0');
239
257
 
240
258
  if (!response.ok) {
241
259
  if (response.status === 401) {
@@ -244,11 +262,10 @@ export async function validateApiKey() {
244
262
  return { valid: false, reason: 'api_error' };
245
263
  }
246
264
 
247
- const data = await response.json();
265
+ // Key is valid if request succeeds
266
+ // Note: synced-todos doesn't return user info, so we mark as valid without email
248
267
  return {
249
- valid: true,
250
- userId: data.user_id,
251
- email: data.email
268
+ valid: true
252
269
  };
253
270
  } catch (error) {
254
271
  return { valid: false, reason: 'network_error', error: error.message };
@@ -322,4 +339,30 @@ export async function getLatestVersion() {
322
339
  }
323
340
  }
324
341
 
342
+ /**
343
+ * Learn vocabulary terms for a task.
344
+ *
345
+ * The LLM determines WHAT keywords to send; this function handles HOW.
346
+ *
347
+ * @param {string} todoId - UUID of the task
348
+ * @param {string[]} keywords - List of vocabulary terms
349
+ * @returns {Promise<Object>} Result with keywords_added, keywords_duplicate, etc.
350
+ */
351
+ export async function learnVocabulary(todoId, keywords) {
352
+ const response = await apiRequest('learn-keywords', {
353
+ method: 'POST',
354
+ body: JSON.stringify({
355
+ todo_id: todoId,
356
+ keywords
357
+ })
358
+ });
359
+
360
+ if (!response.ok) {
361
+ const text = await response.text();
362
+ throw new Error(`Failed to learn vocabulary: ${text}`);
363
+ }
364
+
365
+ return response.json();
366
+ }
367
+
325
368
  export { API_BASE };
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Certainty Analyzer for Push Task Execution
3
+ *
4
+ * Analyzes todos to determine execution certainty based on multiple signals.
5
+ * High-certainty todos execute automatically; low-certainty todos trigger
6
+ * planning mode or prompt for clarification.
7
+ *
8
+ * Architecture:
9
+ * - Input: Task content, context, and metadata
10
+ * - Output: Certainty score (0.0-1.0) + confidence reasons + clarification questions
11
+ *
12
+ * Certainty Signals:
13
+ * 1. Content specificity - Does it clearly describe what to do?
14
+ * 2. Scope clarity - Is the scope well-defined (vs "improve performance")?
15
+ * 3. Codebase context - Does it reference specific files/functions?
16
+ * 4. Action verb presence - Does it start with actionable verbs?
17
+ * 5. Ambiguity markers - Are there question marks, "maybe", "or", etc.?
18
+ *
19
+ * Thresholds:
20
+ * - High certainty (>= 0.7): Execute immediately
21
+ * - Medium certainty (0.4-0.7): Execute with planning mode
22
+ * - Low certainty (< 0.4): Request clarification before execution
23
+ *
24
+ * Ported from: plugins/push-todo/scripts/certainty_analyzer.py
25
+ */
26
+
27
+ // ==================== Constants ====================
28
+
29
+ // High-confidence action verbs (imperative, specific)
30
+ const HIGH_CONFIDENCE_VERBS = [
31
+ 'add', 'create', 'implement', 'fix', 'update', 'remove', 'delete',
32
+ 'rename', 'refactor', 'migrate', 'upgrade', 'install', 'configure',
33
+ 'write', 'modify', 'change', 'replace', 'extract', 'move', 'copy',
34
+ 'integrate', 'connect', 'disconnect', 'enable', 'disable', 'test'
35
+ ];
36
+
37
+ // Low-confidence words (vague, uncertain)
38
+ const LOW_CONFIDENCE_MARKERS = [
39
+ 'maybe', 'possibly', 'might', 'could', 'should consider',
40
+ 'think about', 'explore', 'investigate', 'look into',
41
+ 'try to', 'attempt to', 'see if', 'check if',
42
+ 'or something', 'or maybe', 'not sure', 'unclear',
43
+ 'somehow', 'whatever', 'something like', 'kind of'
44
+ ];
45
+
46
+ // Question patterns that indicate uncertainty
47
+ const QUESTION_PATTERNS = [
48
+ /\?/, // Direct questions
49
+ /\bwhat\s+(should|would|could)\b/i,
50
+ /\bhow\s+(should|would|could|do)\b/i,
51
+ /\bwhich\s+(one|approach|way|method)\b/i,
52
+ /\bis\s+it\s+(better|possible|ok)\b/i
53
+ ];
54
+
55
+ // Specificity indicators (file paths, function names, etc.)
56
+ const SPECIFICITY_PATTERNS = [
57
+ /\b[A-Z][a-zA-Z]+\.(swift|ts|tsx|py|js|jsx|go|rs|java|kt)\b/, // File names
58
+ /\bfunc\s+\w+\b|\bfunction\s+\w+\b|\bdef\s+\w+\b/, // Function defs
59
+ /\bclass\s+[A-Z][a-zA-Z]+\b/, // Class names
60
+ /[a-zA-Z]+\.[a-zA-Z]+\(\)/, // Method calls
61
+ /\/[a-zA-Z][a-zA-Z0-9_/\-.]+/, // File paths
62
+ /\b(line|row)\s+\d+\b/, // Line numbers
63
+ /#\d+/, // Issue/PR numbers
64
+ /\b(error|warning|bug)\s*:\s*\w+/ // Error references
65
+ ];
66
+
67
+ // Scope indicators that suggest well-defined tasks
68
+ const SCOPE_INDICATORS = [
69
+ /\bin\s+(?:the\s+)?(\w+\.\w+|[\w/]+)\b/, // "in file.ts" or "in src/utils"
70
+ /\bfor\s+(?:the\s+)?(\w+)\s+(component|service|module|class|function)\b/,
71
+ /\bwhen\s+\w+/, // Condition-based scope
72
+ /\bonly\s+(for|in|when)\b/ // Explicit scope limitation
73
+ ];
74
+
75
+ // ==================== Certainty Levels ====================
76
+
77
+ export const CertaintyLevel = {
78
+ HIGH: 'high', // >= 0.7: Execute immediately
79
+ MEDIUM: 'medium', // 0.4-0.7: Execute with planning mode
80
+ LOW: 'low' // < 0.4: Request clarification
81
+ };
82
+
83
+ // ==================== CertaintyAnalyzer Class ====================
84
+
85
+ export class CertaintyAnalyzer {
86
+ constructor() {
87
+ // Base score starts at 0.5 (neutral)
88
+ this.baseScore = 0.5;
89
+ }
90
+
91
+ /**
92
+ * Analyze task content and return certainty assessment.
93
+ *
94
+ * @param {string} content - The normalized task content
95
+ * @param {string|null} summary - Optional task summary
96
+ * @param {string|null} transcript - Optional original voice transcript
97
+ * @returns {Object} CertaintyAnalysis with score, level, reasons, and questions
98
+ */
99
+ analyze(content, summary = null, transcript = null) {
100
+ // Combine all text for analysis
101
+ const fullText = this._combineText(content, summary, transcript);
102
+ const fullTextLower = fullText.toLowerCase();
103
+
104
+ const reasons = [];
105
+ const questions = [];
106
+ let score = this.baseScore;
107
+
108
+ // 1. Check for action verbs at the start
109
+ const [verbScore, verbReason] = this._checkActionVerbs(fullTextLower);
110
+ score += verbScore;
111
+ if (verbReason) reasons.push(verbReason);
112
+
113
+ // 2. Check for low-confidence markers
114
+ const [markerScore, markerReasons] = this._checkLowConfidenceMarkers(fullTextLower);
115
+ score += markerScore;
116
+ reasons.push(...markerReasons);
117
+
118
+ // 3. Check for questions/uncertainty
119
+ const [questionScore, questionReason, questionQ] = this._checkQuestions(fullTextLower);
120
+ score += questionScore;
121
+ if (questionReason) reasons.push(questionReason);
122
+ if (questionQ) questions.push(questionQ);
123
+
124
+ // 4. Check for specificity (file names, functions, etc.)
125
+ const [specScore, specReason] = this._checkSpecificity(fullText);
126
+ score += specScore;
127
+ if (specReason) reasons.push(specReason);
128
+
129
+ // 5. Check for scope clarity
130
+ const [scopeScore, scopeReason, scopeQ] = this._checkScope(fullTextLower);
131
+ score += scopeScore;
132
+ if (scopeReason) reasons.push(scopeReason);
133
+ if (scopeQ) questions.push(scopeQ);
134
+
135
+ // 6. Check content length (very short = potentially unclear)
136
+ const [lengthScore, lengthReason, lengthQ] = this._checkContentLength(content);
137
+ score += lengthScore;
138
+ if (lengthReason) reasons.push(lengthReason);
139
+ if (lengthQ) questions.push(lengthQ);
140
+
141
+ // 7. Check for multiple alternatives (indicates decision needed)
142
+ const [altScore, altReason, altQ] = this._checkAlternatives(fullTextLower);
143
+ score += altScore;
144
+ if (altReason) reasons.push(altReason);
145
+ if (altQ) questions.push(altQ);
146
+
147
+ // Clamp score to [0, 1]
148
+ score = Math.max(0.0, Math.min(1.0, score));
149
+
150
+ // Determine level
151
+ let level;
152
+ if (score >= 0.7) {
153
+ level = CertaintyLevel.HIGH;
154
+ } else if (score >= 0.4) {
155
+ level = CertaintyLevel.MEDIUM;
156
+ } else {
157
+ level = CertaintyLevel.LOW;
158
+ }
159
+
160
+ // Sort questions by priority (higher first)
161
+ questions.sort((a, b) => b.priority - a.priority);
162
+
163
+ // Determine recommended action
164
+ const recommendedAction = this._getRecommendedAction(level, questions);
165
+
166
+ return {
167
+ score: Math.round(score * 100) / 100,
168
+ level,
169
+ reasons,
170
+ clarificationQuestions: questions,
171
+ recommendedAction
172
+ };
173
+ }
174
+
175
+ _combineText(content, summary, transcript) {
176
+ const parts = [content];
177
+ if (summary) parts.push(summary);
178
+ if (transcript) parts.push(transcript);
179
+ return parts.join(' ');
180
+ }
181
+
182
+ _checkActionVerbs(text) {
183
+ const words = text.split(/\s+/);
184
+ if (words.length === 0) {
185
+ return [0.0, null];
186
+ }
187
+
188
+ const firstWord = words[0].replace(/[.,!?:;]/g, '');
189
+
190
+ if (HIGH_CONFIDENCE_VERBS.includes(firstWord)) {
191
+ return [0.15, {
192
+ factor: 'action_verb',
193
+ scoreDelta: 0.15,
194
+ explanation: `Starts with clear action verb: '${firstWord}'`
195
+ }];
196
+ }
197
+
198
+ // Check first 5 words for verb
199
+ for (const word of words.slice(0, 5)) {
200
+ const cleaned = word.replace(/[.,!?:;]/g, '');
201
+ if (HIGH_CONFIDENCE_VERBS.includes(cleaned)) {
202
+ return [0.08, {
203
+ factor: 'action_verb',
204
+ scoreDelta: 0.08,
205
+ explanation: `Contains action verb: '${cleaned}'`
206
+ }];
207
+ }
208
+ }
209
+
210
+ return [-0.1, {
211
+ factor: 'action_verb',
212
+ scoreDelta: -0.1,
213
+ explanation: 'No clear action verb found'
214
+ }];
215
+ }
216
+
217
+ _checkLowConfidenceMarkers(text) {
218
+ const reasons = [];
219
+ let totalDelta = 0.0;
220
+
221
+ for (const marker of LOW_CONFIDENCE_MARKERS) {
222
+ if (text.includes(marker)) {
223
+ const delta = -0.1;
224
+ totalDelta += delta;
225
+ reasons.push({
226
+ factor: 'uncertainty_marker',
227
+ scoreDelta: delta,
228
+ explanation: `Contains uncertainty marker: '${marker}'`
229
+ });
230
+ // Cap at -0.3 total
231
+ if (totalDelta <= -0.3) {
232
+ break;
233
+ }
234
+ }
235
+ }
236
+
237
+ return [totalDelta, reasons];
238
+ }
239
+
240
+ _checkQuestions(text) {
241
+ for (const pattern of QUESTION_PATTERNS) {
242
+ if (pattern.test(text)) {
243
+ return [-0.15, {
244
+ factor: 'question_present',
245
+ scoreDelta: -0.15,
246
+ explanation: 'Task contains questions or uncertainty'
247
+ }, {
248
+ question: 'The task seems to ask a question. Can you clarify what action to take?',
249
+ options: ['Investigate and recommend', 'Make a decision for me', 'Skip this task'],
250
+ priority: 2
251
+ }];
252
+ }
253
+ }
254
+ return [0.0, null, null];
255
+ }
256
+
257
+ _checkSpecificity(text) {
258
+ const matches = [];
259
+ for (const pattern of SPECIFICITY_PATTERNS) {
260
+ const found = text.match(pattern);
261
+ if (found) {
262
+ matches.push(found[0]);
263
+ }
264
+ }
265
+
266
+ if (matches.length >= 3) {
267
+ return [0.2, {
268
+ factor: 'high_specificity',
269
+ scoreDelta: 0.2,
270
+ explanation: `Multiple specific references found (${matches.length} items)`
271
+ }];
272
+ } else if (matches.length >= 1) {
273
+ return [0.1, {
274
+ factor: 'specificity',
275
+ scoreDelta: 0.1,
276
+ explanation: `Contains specific references: ${matches.slice(0, 3).join(', ')}`
277
+ }];
278
+ }
279
+
280
+ return [-0.05, {
281
+ factor: 'low_specificity',
282
+ scoreDelta: -0.05,
283
+ explanation: 'No specific file/function references found'
284
+ }];
285
+ }
286
+
287
+ _checkScope(text) {
288
+ const hasScope = SCOPE_INDICATORS.some(pattern => pattern.test(text));
289
+
290
+ // Check for overly broad terms
291
+ const broadTerms = ['everything', 'all files', 'entire', 'whole codebase',
292
+ 'the system', 'performance', 'improve'];
293
+ const hasBroad = broadTerms.some(term => text.includes(term));
294
+
295
+ if (hasScope && !hasBroad) {
296
+ return [0.1, {
297
+ factor: 'clear_scope',
298
+ scoreDelta: 0.1,
299
+ explanation: 'Task has well-defined scope'
300
+ }, null];
301
+ } else if (hasBroad) {
302
+ return [-0.15, {
303
+ factor: 'broad_scope',
304
+ scoreDelta: -0.15,
305
+ explanation: 'Task scope is very broad'
306
+ }, {
307
+ question: 'The task scope seems broad. Can you narrow it down?',
308
+ options: ['Focus on most critical area', 'Start with a specific file',
309
+ 'Analyze first, then decide'],
310
+ priority: 3
311
+ }];
312
+ }
313
+
314
+ return [0.0, null, null];
315
+ }
316
+
317
+ _checkContentLength(content) {
318
+ const words = content.split(/\s+/).filter(w => w.length > 0).length;
319
+
320
+ if (words < 5) {
321
+ return [-0.2, {
322
+ factor: 'very_short',
323
+ scoreDelta: -0.2,
324
+ explanation: `Task description very short (${words} words)`
325
+ }, {
326
+ question: 'Can you provide more detail about what specifically needs to be done?',
327
+ options: [],
328
+ priority: 4
329
+ }];
330
+ } else if (words < 10) {
331
+ return [-0.05, {
332
+ factor: 'short',
333
+ scoreDelta: -0.05,
334
+ explanation: `Task description brief (${words} words)`
335
+ }, null];
336
+ } else if (words > 50) {
337
+ return [0.1, {
338
+ factor: 'detailed',
339
+ scoreDelta: 0.1,
340
+ explanation: 'Task has detailed description'
341
+ }, null];
342
+ }
343
+
344
+ return [0.0, null, null];
345
+ }
346
+
347
+ _checkAlternatives(text) {
348
+ const altPatterns = [
349
+ /\bor\s+\w+\b.*\bor\s+\w+\b/i, // Multiple "or"s
350
+ /\beither\b.*\bor\b/i,
351
+ /option\s*[a-d1-4]/i,
352
+ /\balternative(ly)?\b/i,
353
+ /\bversus\b|\bvs\.?\b/i
354
+ ];
355
+
356
+ for (const pattern of altPatterns) {
357
+ if (pattern.test(text)) {
358
+ return [-0.15, {
359
+ factor: 'multiple_alternatives',
360
+ scoreDelta: -0.15,
361
+ explanation: 'Task presents multiple alternatives'
362
+ }, {
363
+ question: 'Which approach should I take?',
364
+ options: [],
365
+ priority: 5
366
+ }];
367
+ }
368
+ }
369
+
370
+ return [0.0, null, null];
371
+ }
372
+
373
+ _getRecommendedAction(level, questions) {
374
+ if (level === CertaintyLevel.HIGH) {
375
+ return 'execute';
376
+ } else if (level === CertaintyLevel.MEDIUM) {
377
+ return 'execute_with_plan';
378
+ } else {
379
+ if (questions.length > 0) {
380
+ return 'clarify';
381
+ }
382
+ return 'skip_or_clarify';
383
+ }
384
+ }
385
+ }
386
+
387
+ // ==================== Convenience Functions ====================
388
+
389
+ /**
390
+ * Convenience function to analyze task certainty.
391
+ *
392
+ * @param {string} content - The normalized task content
393
+ * @param {string|null} summary - Optional task summary
394
+ * @param {string|null} transcript - Optional original voice transcript
395
+ * @returns {Object} CertaintyAnalysis with score, level, reasons, and questions
396
+ */
397
+ export function analyzeCertainty(content, summary = null, transcript = null) {
398
+ const analyzer = new CertaintyAnalyzer();
399
+ return analyzer.analyze(content, summary, transcript);
400
+ }
401
+
402
+ /**
403
+ * Quick check if task should be executed.
404
+ *
405
+ * @param {string} content - The normalized task content
406
+ * @param {string|null} summary - Optional task summary
407
+ * @param {string|null} transcript - Optional original voice transcript
408
+ * @param {number} threshold - Minimum certainty score (default 0.4 = medium confidence)
409
+ * @returns {boolean} True if task certainty meets threshold
410
+ */
411
+ export function shouldExecute(content, summary = null, transcript = null, threshold = 0.4) {
412
+ const analysis = analyzeCertainty(content, summary, transcript);
413
+ return analysis.score >= threshold;
414
+ }
415
+
416
+ /**
417
+ * Get execution mode based on certainty.
418
+ *
419
+ * @param {Object} analysis - CertaintyAnalysis result
420
+ * @returns {string} 'immediate' | 'planning' | 'clarify'
421
+ */
422
+ export function getExecutionMode(analysis) {
423
+ if (!analysis) {
424
+ return 'immediate';
425
+ }
426
+
427
+ if (analysis.score >= 0.7) {
428
+ return 'immediate';
429
+ } else if (analysis.score >= 0.4) {
430
+ return 'planning';
431
+ } else {
432
+ return 'clarify';
433
+ }
434
+ }