@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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/push-keychain-helper +0 -0
- package/hooks/session-end.js +1 -1
- package/hooks/session-start.js +61 -4
- package/lib/api.js +59 -16
- package/lib/certainty.js +434 -0
- package/lib/cli.js +310 -4
- package/lib/connect.js +1120 -200
- package/lib/daemon-health.js +193 -0
- package/lib/daemon.js +1369 -0
- package/lib/fetch.js +16 -1
- package/lib/utils/git.js +43 -0
- package/lib/utils/screenshots.js +65 -0
- package/lib/watch.js +13 -2
- package/natives/KeychainHelper.swift +310 -93
- package/package.json +2 -1
- package/scripts/postinstall.js +306 -14
- package/scripts/preuninstall.js +66 -0
|
Binary file
|
package/hooks/session-end.js
CHANGED
|
@@ -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+
|
|
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;
|
package/hooks/session-start.js
CHANGED
|
@@ -2,16 +2,67 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Session start hook for Push CLI.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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+
|
|
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 -
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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('
|
|
145
|
-
method: '
|
|
155
|
+
const response = await apiRequest('update-task-execution', {
|
|
156
|
+
method: 'PATCH',
|
|
146
157
|
body: JSON.stringify({
|
|
147
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/certainty.js
ADDED
|
@@ -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
|
+
}
|