@masslessai/push-todo 4.1.7 → 4.1.9
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/lib/api.js +140 -0
- package/lib/cli.js +100 -0
- package/lib/daemon.js +68 -2
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -381,4 +381,144 @@ export async function learnVocabulary(todoId, keywords) {
|
|
|
381
381
|
return response.json();
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
// ==================== Phase 3: Extended Skill CLI ====================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Report structured progress for a running task.
|
|
388
|
+
* Called by the agent during execution to push activity updates to Supabase.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} todoId - UUID of the task
|
|
391
|
+
* @param {Object} options - Progress details
|
|
392
|
+
* @param {string} options.phase - Current phase (e.g., "testing", "implementing")
|
|
393
|
+
* @param {string} [options.detail] - Human-readable detail
|
|
394
|
+
* @returns {Promise<Object>}
|
|
395
|
+
*/
|
|
396
|
+
export async function reportProgress(todoId, { phase, detail }) {
|
|
397
|
+
const response = await apiRequest('update-task-execution', {
|
|
398
|
+
method: 'PATCH',
|
|
399
|
+
body: JSON.stringify({
|
|
400
|
+
todoId,
|
|
401
|
+
event: {
|
|
402
|
+
type: 'progress',
|
|
403
|
+
timestamp: new Date().toISOString(),
|
|
404
|
+
summary: detail ? `${phase}: ${detail}` : phase,
|
|
405
|
+
phase
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
const text = await response.text();
|
|
412
|
+
throw new Error(`Failed to report progress: ${text}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return response.json();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Update execution status/phase for a running task.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} todoId - UUID of the task
|
|
422
|
+
* @param {string} status - New phase label (e.g., "implementing", "testing", "reviewing")
|
|
423
|
+
* @returns {Promise<Object>}
|
|
424
|
+
*/
|
|
425
|
+
export async function updateStatus(todoId, status) {
|
|
426
|
+
const response = await apiRequest('update-task-execution', {
|
|
427
|
+
method: 'PATCH',
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
todoId,
|
|
430
|
+
event: {
|
|
431
|
+
type: 'progress',
|
|
432
|
+
timestamp: new Date().toISOString(),
|
|
433
|
+
summary: `Phase: ${status}`
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const text = await response.text();
|
|
440
|
+
throw new Error(`Failed to update status: ${text}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return response.json();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Check for messages from the human for a running task.
|
|
448
|
+
* Returns pending messages or empty array.
|
|
449
|
+
*
|
|
450
|
+
* @param {string} todoId - UUID of the task
|
|
451
|
+
* @returns {Promise<Object[]>} Array of pending messages
|
|
452
|
+
*/
|
|
453
|
+
export async function checkMessages(todoId) {
|
|
454
|
+
const params = new URLSearchParams({ todo_id: todoId, direction: 'to_agent' });
|
|
455
|
+
const response = await apiRequest(`task-messages?${params}`, {
|
|
456
|
+
method: 'GET'
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
// 404 = no task_messages table/function yet — return empty gracefully
|
|
461
|
+
if (response.status === 404) return [];
|
|
462
|
+
const text = await response.text();
|
|
463
|
+
throw new Error(`Failed to check messages: ${text}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const result = await response.json();
|
|
467
|
+
return result.messages || [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Request input from the human for a running task.
|
|
472
|
+
* Posts a question and polls until the human responds or timeout.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} todoId - UUID of the task
|
|
475
|
+
* @param {string} question - Question to ask the human
|
|
476
|
+
* @param {number} [timeoutMs=300000] - Timeout in ms (default 5 min)
|
|
477
|
+
* @returns {Promise<string|null>} Human's response or null if timeout
|
|
478
|
+
*/
|
|
479
|
+
export async function requestInput(todoId, question, timeoutMs = 300000) {
|
|
480
|
+
// Post the question
|
|
481
|
+
const postResponse = await apiRequest('task-messages', {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
body: JSON.stringify({
|
|
484
|
+
todo_id: todoId,
|
|
485
|
+
direction: 'to_human',
|
|
486
|
+
message: question,
|
|
487
|
+
type: 'input_request'
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
if (!postResponse.ok) {
|
|
492
|
+
// 404 = task-messages endpoint not deployed yet
|
|
493
|
+
if (postResponse.status === 404) {
|
|
494
|
+
console.error('Message system not available yet. Use [push-confirm] pattern instead.');
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const text = await postResponse.text();
|
|
498
|
+
throw new Error(`Failed to request input: ${text}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const { message_id } = await postResponse.json();
|
|
502
|
+
|
|
503
|
+
// Poll for response
|
|
504
|
+
const startTime = Date.now();
|
|
505
|
+
const pollInterval = 5000; // 5s
|
|
506
|
+
|
|
507
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
508
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
509
|
+
|
|
510
|
+
const checkResponse = await apiRequest(`task-messages?message_id=${message_id}&direction=to_agent`, {
|
|
511
|
+
method: 'GET'
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (checkResponse.ok) {
|
|
515
|
+
const result = await checkResponse.json();
|
|
516
|
+
const reply = (result.messages || []).find(m => m.in_reply_to === message_id);
|
|
517
|
+
if (reply) return reply.message;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return null; // Timeout
|
|
522
|
+
}
|
|
523
|
+
|
|
384
524
|
export { API_BASE };
|
package/lib/cli.js
CHANGED
|
@@ -68,6 +68,13 @@ ${bold('OPTIONS:')}
|
|
|
68
68
|
--view-screenshot <idx> Open screenshot for viewing (index or filename)
|
|
69
69
|
--learn-vocabulary <uuid> Contribute vocabulary for a task
|
|
70
70
|
--keywords <terms> Comma-separated vocabulary terms (with --learn-vocabulary)
|
|
71
|
+
--report-progress <uuid> Report progress for a running task
|
|
72
|
+
--phase <name> Phase name (with --report-progress)
|
|
73
|
+
--detail <text> Detail text (with --report-progress)
|
|
74
|
+
--update-status <uuid> Update execution phase for a running task
|
|
75
|
+
--check-messages <uuid> Check for messages from the human
|
|
76
|
+
--request-input <uuid> Request input from the human (blocking)
|
|
77
|
+
--question <text> Question to ask (with --request-input)
|
|
71
78
|
--set-batch-size <N> Set max tasks for batch queue (1-20)
|
|
72
79
|
--daemon-status Show daemon status
|
|
73
80
|
--daemon-start Start daemon manually
|
|
@@ -175,6 +182,14 @@ const options = {
|
|
|
175
182
|
'create-todo': { type: 'string' },
|
|
176
183
|
'notify': { type: 'string' },
|
|
177
184
|
'queue-execution': { type: 'string' },
|
|
185
|
+
// Skill CLI options (Phase 3)
|
|
186
|
+
'report-progress': { type: 'string' },
|
|
187
|
+
'phase': { type: 'string' },
|
|
188
|
+
'detail': { type: 'string' },
|
|
189
|
+
'update-status': { type: 'string' },
|
|
190
|
+
'check-messages': { type: 'string' },
|
|
191
|
+
'request-input': { type: 'string' },
|
|
192
|
+
'question': { type: 'string' },
|
|
178
193
|
};
|
|
179
194
|
|
|
180
195
|
/**
|
|
@@ -512,6 +527,91 @@ export async function run(argv) {
|
|
|
512
527
|
return;
|
|
513
528
|
}
|
|
514
529
|
|
|
530
|
+
// Handle --report-progress (report structured progress for a running task)
|
|
531
|
+
if (values['report-progress']) {
|
|
532
|
+
try {
|
|
533
|
+
const result = await api.reportProgress(values['report-progress'], {
|
|
534
|
+
phase: values.phase || 'progress',
|
|
535
|
+
detail: values.detail || undefined,
|
|
536
|
+
});
|
|
537
|
+
if (values.json) {
|
|
538
|
+
console.log(JSON.stringify(result, null, 2));
|
|
539
|
+
} else {
|
|
540
|
+
console.log(green('Progress reported.'));
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error(red(`Failed to report progress: ${error.message}`));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Handle --update-status (update execution phase for a running task)
|
|
550
|
+
if (values['update-status']) {
|
|
551
|
+
const status = positionals[0] || values.phase;
|
|
552
|
+
if (!status) {
|
|
553
|
+
console.error(red('Status required. Usage: --update-status <uuid> <status>'));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const result = await api.updateStatus(values['update-status'], status);
|
|
558
|
+
if (values.json) {
|
|
559
|
+
console.log(JSON.stringify(result, null, 2));
|
|
560
|
+
} else {
|
|
561
|
+
console.log(green(`Status updated to: ${status}`));
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.error(red(`Failed to update status: ${error.message}`));
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Handle --check-messages (check for messages from the human)
|
|
571
|
+
if (values['check-messages']) {
|
|
572
|
+
try {
|
|
573
|
+
const messages = await api.checkMessages(values['check-messages']);
|
|
574
|
+
if (values.json) {
|
|
575
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
576
|
+
} else if (messages.length === 0) {
|
|
577
|
+
console.log(dim('No pending messages.'));
|
|
578
|
+
} else {
|
|
579
|
+
for (const msg of messages) {
|
|
580
|
+
console.log(`[${msg.created_at || 'unknown'}] ${msg.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error(red(`Failed to check messages: ${error.message}`));
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Handle --request-input (ask the human a question, poll for response)
|
|
591
|
+
if (values['request-input']) {
|
|
592
|
+
const question = values.question || positionals.slice(0).join(' ');
|
|
593
|
+
if (!question) {
|
|
594
|
+
console.error(red('--question required with --request-input'));
|
|
595
|
+
console.error('Usage: --request-input <uuid> --question "What should I do?"');
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
console.log(dim('Waiting for human response (timeout: 5 min)...'));
|
|
600
|
+
const reply = await api.requestInput(values['request-input'], question);
|
|
601
|
+
if (values.json) {
|
|
602
|
+
console.log(JSON.stringify({ reply }, null, 2));
|
|
603
|
+
} else if (reply) {
|
|
604
|
+
console.log(green('Response:'), reply);
|
|
605
|
+
} else {
|
|
606
|
+
console.log(dim('No response received (timeout).'));
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(red(`Failed to request input: ${error.message}`));
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
515
615
|
// Auto-start daemon on every command (self-healing behavior)
|
|
516
616
|
ensureDaemonRunning();
|
|
517
617
|
|
package/lib/daemon.js
CHANGED
|
@@ -1482,11 +1482,38 @@ function maybesSendStreamProgress(displayNumber, activity) {
|
|
|
1482
1482
|
filesRead: activity.filesRead.size
|
|
1483
1483
|
}
|
|
1484
1484
|
})
|
|
1485
|
+
}).then(async (response) => {
|
|
1486
|
+
// Check for cancellation signal in response
|
|
1487
|
+
const result = await response.json().catch(() => null);
|
|
1488
|
+
if (result?.status === 'cancelling') {
|
|
1489
|
+
handleCancellation(displayNumber, 'stream_progress');
|
|
1490
|
+
}
|
|
1485
1491
|
}).catch(err => {
|
|
1486
1492
|
log(`Task #${displayNumber}: stream progress failed (non-fatal): ${err.message}`);
|
|
1487
1493
|
});
|
|
1488
1494
|
}
|
|
1489
1495
|
|
|
1496
|
+
// ==================== Cancellation (Phase 2) ====================
|
|
1497
|
+
|
|
1498
|
+
function handleCancellation(displayNumber, source) {
|
|
1499
|
+
const taskInfo = runningTasks.get(displayNumber);
|
|
1500
|
+
if (!taskInfo) return;
|
|
1501
|
+
|
|
1502
|
+
log(`Task #${displayNumber}: cancellation detected via ${source}, sending SIGTERM`);
|
|
1503
|
+
|
|
1504
|
+
try {
|
|
1505
|
+
taskInfo.process.kill('SIGTERM');
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
log(`Task #${displayNumber}: SIGTERM failed: ${err.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Mark as cancelled so handleTaskCompletion reports the right reason
|
|
1511
|
+
updateTaskDetail(displayNumber, {
|
|
1512
|
+
phase: 'cancelled',
|
|
1513
|
+
detail: 'Cancelled by user'
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1490
1517
|
function monitorTaskStdout(displayNumber, proc) {
|
|
1491
1518
|
if (!proc.stdout) return;
|
|
1492
1519
|
|
|
@@ -1623,6 +1650,12 @@ async function sendProgressHeartbeats() {
|
|
|
1623
1650
|
}
|
|
1624
1651
|
// No status field — event-only update, won't change execution_status
|
|
1625
1652
|
})
|
|
1653
|
+
}).then(async (response) => {
|
|
1654
|
+
// Check for cancellation signal in heartbeat response
|
|
1655
|
+
const result = await response.json().catch(() => null);
|
|
1656
|
+
if (result?.status === 'cancelling') {
|
|
1657
|
+
handleCancellation(displayNumber, 'heartbeat');
|
|
1658
|
+
}
|
|
1626
1659
|
}).catch(err => {
|
|
1627
1660
|
log(`Task #${displayNumber}: heartbeat failed (non-fatal): ${err.message}`);
|
|
1628
1661
|
});
|
|
@@ -2162,12 +2195,45 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
2162
2195
|
const summary = info.summary || 'Unknown task';
|
|
2163
2196
|
const projectPath = taskProjectPaths.get(displayNumber);
|
|
2164
2197
|
|
|
2165
|
-
|
|
2198
|
+
const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
2199
|
+
const wasCancelled = info.phase === 'cancelled';
|
|
2200
|
+
log(`Task #${displayNumber} ${wasCancelled ? 'cancelled' : 'completed'} with code ${exitCode} (${duration}s)`);
|
|
2201
|
+
|
|
2202
|
+
// Handle user cancellation — report as failed with clear reason, skip PR/merge/summary
|
|
2203
|
+
if (wasCancelled) {
|
|
2204
|
+
const cancelMsg = `Cancelled by user after ${durationStr}.`;
|
|
2205
|
+
await updateTaskStatus(displayNumber, 'failed', {
|
|
2206
|
+
error: cancelMsg,
|
|
2207
|
+
sessionId: taskInfo.sessionId
|
|
2208
|
+
}, info.taskId);
|
|
2209
|
+
|
|
2210
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
2211
|
+
|
|
2212
|
+
trackCompleted({
|
|
2213
|
+
displayNumber,
|
|
2214
|
+
summary,
|
|
2215
|
+
completedAt: new Date().toISOString(),
|
|
2216
|
+
duration,
|
|
2217
|
+
status: 'cancelled'
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
// Cleanup internal tracking
|
|
2221
|
+
taskDetails.delete(displayNumber);
|
|
2222
|
+
taskLastOutput.delete(displayNumber);
|
|
2223
|
+
taskStdoutBuffer.delete(displayNumber);
|
|
2224
|
+
taskStderrBuffer.delete(displayNumber);
|
|
2225
|
+
taskProjectPaths.delete(displayNumber);
|
|
2226
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
2227
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2228
|
+
taskActivityState.delete(displayNumber);
|
|
2229
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
2230
|
+
updateStatusFile();
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2166
2233
|
|
|
2167
2234
|
// Session ID: prefer pre-assigned ID (reliable), fall back to stdout extraction
|
|
2168
2235
|
const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || [], displayNumber);
|
|
2169
2236
|
const worktreePath = getWorktreePath(displayNumber, projectPath);
|
|
2170
|
-
const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
2171
2237
|
const machineName = getMachineName() || 'Mac';
|
|
2172
2238
|
|
|
2173
2239
|
if (sessionId) {
|