@masslessai/push-todo 3.10.5 → 3.10.7
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/SKILL.md +10 -1
- package/hooks/session-end.js +1 -0
- package/lib/daemon.js +33 -22
- package/lib/fetch.js +1 -0
- package/lib/utils/format.js +6 -0
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -348,6 +348,16 @@ If the session file cannot be found (daemon just started, no output yet):
|
|
|
348
348
|
|
|
349
349
|
Tasks can have **screenshot images** and **reference links** attached. When working on a task, always check for and use these attachments — they provide critical visual and reference context.
|
|
350
350
|
|
|
351
|
+
### Context App
|
|
352
|
+
|
|
353
|
+
The task output may show a **Context App** field (e.g., `X`, `Safari`, `WeChat`, `Slack`). This tells you which app the user was in when they created the voice task. Use this as semantic context:
|
|
354
|
+
- **Context App: X** + screenshot → the screenshot is likely a tweet or thread
|
|
355
|
+
- **Context App: Safari** + link → the user was browsing a webpage
|
|
356
|
+
- **Context App: Slack** → the task relates to a Slack conversation
|
|
357
|
+
- If the field is absent or empty, the user created the task from the Push app directly
|
|
358
|
+
|
|
359
|
+
This context helps interpret ambiguous voice transcripts like "fix this" or "do we need this?" — the context app + screenshot together reveal what "this" refers to.
|
|
360
|
+
|
|
351
361
|
### Screenshot Attachments
|
|
352
362
|
|
|
353
363
|
Screenshots are images captured from the user's phone screen when creating the task. They're stored in **iCloud Documents** and synced to the Mac automatically.
|
|
@@ -381,7 +391,6 @@ Links are reference URLs the user shared when creating the task (e.g., from Safa
|
|
|
381
391
|
|
|
382
392
|
1. Links are displayed as clickable markdown: `🔗 [Title](URL)`
|
|
383
393
|
2. **Use WebFetch** to read the linked content when relevant to the task
|
|
384
|
-
3. The `contextApp` field shows which app the link came from (e.g., "Safari", "WeChat")
|
|
385
394
|
|
|
386
395
|
### Programmatic Access (JSON mode)
|
|
387
396
|
|
package/hooks/session-end.js
CHANGED
package/lib/daemon.js
CHANGED
|
@@ -406,13 +406,17 @@ async function fetchQueuedTasks() {
|
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
async function updateTaskStatus(displayNumber, status, extra = {}) {
|
|
409
|
+
async function updateTaskStatus(displayNumber, status, extra = {}, todoId = null) {
|
|
410
410
|
try {
|
|
411
411
|
const payload = {
|
|
412
412
|
displayNumber,
|
|
413
413
|
status,
|
|
414
414
|
...extra
|
|
415
415
|
};
|
|
416
|
+
// Prefer UUID for API lookup (displayNumber kept for backward compat)
|
|
417
|
+
if (todoId) {
|
|
418
|
+
payload.todoId = todoId;
|
|
419
|
+
}
|
|
416
420
|
|
|
417
421
|
const machineId = getMachineId();
|
|
418
422
|
const machineName = getMachineName();
|
|
@@ -460,7 +464,7 @@ async function updateTaskStatus(displayNumber, status, extra = {}) {
|
|
|
460
464
|
}
|
|
461
465
|
}
|
|
462
466
|
|
|
463
|
-
async function claimTask(displayNumber) {
|
|
467
|
+
async function claimTask(displayNumber, todoId) {
|
|
464
468
|
const machineId = getMachineId();
|
|
465
469
|
const machineName = getMachineName();
|
|
466
470
|
|
|
@@ -473,12 +477,18 @@ async function claimTask(displayNumber) {
|
|
|
473
477
|
const branch = `push-${displayNumber}-${suffix}`;
|
|
474
478
|
|
|
475
479
|
const payload = {
|
|
476
|
-
|
|
480
|
+
todoId: todoId || undefined, // UUID lookup (primary, avoids display_number collisions)
|
|
481
|
+
displayNumber, // Fallback + kept for logging
|
|
477
482
|
status: 'running',
|
|
478
483
|
machineId,
|
|
479
484
|
machineName,
|
|
480
485
|
branch,
|
|
481
|
-
atomic: true
|
|
486
|
+
atomic: true,
|
|
487
|
+
event: {
|
|
488
|
+
type: 'started',
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
machineName: machineName || undefined,
|
|
491
|
+
}
|
|
482
492
|
};
|
|
483
493
|
|
|
484
494
|
try {
|
|
@@ -1276,6 +1286,9 @@ async function executeTask(task) {
|
|
|
1276
1286
|
return null;
|
|
1277
1287
|
}
|
|
1278
1288
|
|
|
1289
|
+
// Extract UUID early — needed for claim and all status updates
|
|
1290
|
+
const taskId = task.id || task.todo_id || '';
|
|
1291
|
+
|
|
1279
1292
|
// Get project path (use action_type for multi-agent routing)
|
|
1280
1293
|
let projectPath = null;
|
|
1281
1294
|
if (gitRemote) {
|
|
@@ -1290,7 +1303,7 @@ async function executeTask(task) {
|
|
|
1290
1303
|
logError(`Task #${displayNumber}: Project path does not exist: ${projectPath}`);
|
|
1291
1304
|
await updateTaskStatus(displayNumber, 'failed', {
|
|
1292
1305
|
error: `Project path not found: ${projectPath}`
|
|
1293
|
-
});
|
|
1306
|
+
}, taskId);
|
|
1294
1307
|
return null;
|
|
1295
1308
|
}
|
|
1296
1309
|
|
|
@@ -1298,13 +1311,10 @@ async function executeTask(task) {
|
|
|
1298
1311
|
}
|
|
1299
1312
|
|
|
1300
1313
|
// Atomic task claiming - must await to actually check the result
|
|
1301
|
-
if (!(await claimTask(displayNumber))) {
|
|
1314
|
+
if (!(await claimTask(displayNumber, taskId))) {
|
|
1302
1315
|
log(`Task #${displayNumber}: claim failed, skipping`);
|
|
1303
1316
|
return null;
|
|
1304
1317
|
}
|
|
1305
|
-
|
|
1306
|
-
// Auto-heal: check if previous execution already completed work for this task
|
|
1307
|
-
const taskId = task.id || task.todo_id || '';
|
|
1308
1318
|
const healed = await autoHealExistingWork(displayNumber, summary, projectPath, taskId);
|
|
1309
1319
|
if (healed) {
|
|
1310
1320
|
log(`Task #${displayNumber}: auto-healed from previous execution, skipping re-execution`);
|
|
@@ -1313,7 +1323,7 @@ async function executeTask(task) {
|
|
|
1313
1323
|
|
|
1314
1324
|
// Track task details
|
|
1315
1325
|
updateTaskDetail(displayNumber, {
|
|
1316
|
-
taskId
|
|
1326
|
+
taskId,
|
|
1317
1327
|
summary,
|
|
1318
1328
|
status: 'running',
|
|
1319
1329
|
phase: 'starting',
|
|
@@ -1327,7 +1337,7 @@ async function executeTask(task) {
|
|
|
1327
1337
|
// Create worktree
|
|
1328
1338
|
const worktreePath = createWorktree(displayNumber, projectPath);
|
|
1329
1339
|
if (!worktreePath) {
|
|
1330
|
-
await updateTaskStatus(displayNumber, 'failed', { error: 'Failed to create git worktree' });
|
|
1340
|
+
await updateTaskStatus(displayNumber, 'failed', { error: 'Failed to create git worktree' }, taskId);
|
|
1331
1341
|
taskDetails.delete(displayNumber);
|
|
1332
1342
|
return null;
|
|
1333
1343
|
}
|
|
@@ -1367,11 +1377,11 @@ IMPORTANT:
|
|
|
1367
1377
|
const child = spawn('claude', claudeArgs, {
|
|
1368
1378
|
cwd: worktreePath,
|
|
1369
1379
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1370
|
-
env: {
|
|
1371
|
-
...process.env,
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
}
|
|
1380
|
+
env: (() => {
|
|
1381
|
+
const env = { ...process.env, PUSH_TASK_ID: task.id, PUSH_DISPLAY_NUMBER: String(displayNumber) };
|
|
1382
|
+
delete env.CLAUDECODE; // Strip to avoid "nested session" guard in Claude Code
|
|
1383
|
+
return env;
|
|
1384
|
+
})()
|
|
1375
1385
|
});
|
|
1376
1386
|
|
|
1377
1387
|
const taskInfo = {
|
|
@@ -1425,7 +1435,7 @@ IMPORTANT:
|
|
|
1425
1435
|
child.on('error', async (error) => {
|
|
1426
1436
|
logError(`Task #${displayNumber} error: ${error.message}`);
|
|
1427
1437
|
runningTasks.delete(displayNumber);
|
|
1428
|
-
await updateTaskStatus(displayNumber, 'failed', { error: error.message });
|
|
1438
|
+
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1429
1439
|
taskDetails.delete(displayNumber);
|
|
1430
1440
|
updateStatusFile();
|
|
1431
1441
|
});
|
|
@@ -1441,7 +1451,7 @@ IMPORTANT:
|
|
|
1441
1451
|
return taskInfo;
|
|
1442
1452
|
} catch (error) {
|
|
1443
1453
|
logError(`Error starting Claude for task #${displayNumber}: ${error.message}`);
|
|
1444
|
-
await updateTaskStatus(displayNumber, 'failed', { error: error.message });
|
|
1454
|
+
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1445
1455
|
taskDetails.delete(displayNumber);
|
|
1446
1456
|
return null;
|
|
1447
1457
|
}
|
|
@@ -1499,13 +1509,13 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1499
1509
|
duration,
|
|
1500
1510
|
sessionId,
|
|
1501
1511
|
summary: executionSummary
|
|
1502
|
-
});
|
|
1512
|
+
}, info.taskId);
|
|
1503
1513
|
if (!statusUpdated) {
|
|
1504
1514
|
logError(`Task #${displayNumber}: Failed to update status to session_finished — will retry`);
|
|
1505
1515
|
// Retry once
|
|
1506
1516
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
1507
1517
|
duration, sessionId, summary: executionSummary
|
|
1508
|
-
});
|
|
1518
|
+
}, info.taskId);
|
|
1509
1519
|
}
|
|
1510
1520
|
|
|
1511
1521
|
if (NOTIFY_ON_COMPLETE) {
|
|
@@ -1581,7 +1591,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1581
1591
|
? `${failureSummary}\nExit code ${exitCode}. Ran for ${durationStr} on ${machineName}.`
|
|
1582
1592
|
: `Exit code ${exitCode}: ${stderr.slice(0, 200)}`;
|
|
1583
1593
|
|
|
1584
|
-
await updateTaskStatus(displayNumber, 'failed', { error: errorMsg });
|
|
1594
|
+
await updateTaskStatus(displayNumber, 'failed', { error: errorMsg }, info.taskId);
|
|
1585
1595
|
|
|
1586
1596
|
if (NOTIFY_ON_FAILURE) {
|
|
1587
1597
|
sendMacNotification(
|
|
@@ -1879,6 +1889,7 @@ async function recoverOrphanedTasks() {
|
|
|
1879
1889
|
|
|
1880
1890
|
for (const task of orphaned) {
|
|
1881
1891
|
const dn = task.displayNumber || task.display_number;
|
|
1892
|
+
const tid = task.id || task.todo_id || null;
|
|
1882
1893
|
log(`Task #${dn}: resetting from 'running' to 'queued' (orphaned by restart)`);
|
|
1883
1894
|
await updateTaskStatus(dn, 'queued', {
|
|
1884
1895
|
event: {
|
|
@@ -1887,7 +1898,7 @@ async function recoverOrphanedTasks() {
|
|
|
1887
1898
|
machineName: getMachineName() || undefined,
|
|
1888
1899
|
summary: 'Daemon restarted — re-queuing for auto-heal'
|
|
1889
1900
|
}
|
|
1890
|
-
});
|
|
1901
|
+
}, tid);
|
|
1891
1902
|
}
|
|
1892
1903
|
} catch (error) {
|
|
1893
1904
|
log(`Orphaned task recovery failed (non-fatal): ${error.message}`);
|
package/lib/fetch.js
CHANGED
|
@@ -250,6 +250,7 @@ async function trackActiveTask(task) {
|
|
|
250
250
|
const machineId = getMachineId();
|
|
251
251
|
const machineName = getMachineName();
|
|
252
252
|
await api.updateTaskExecution({
|
|
253
|
+
todoId: task.id || undefined,
|
|
253
254
|
displayNumber: task.displayNumber,
|
|
254
255
|
status: 'running',
|
|
255
256
|
machineId,
|
package/lib/utils/format.js
CHANGED
|
@@ -176,6 +176,12 @@ export function formatTaskForDisplay(task) {
|
|
|
176
176
|
lines.push(`**Session:** Resumable (\`push-todo resume ${displayNum}\`) - continues the exact Claude Code conversation`);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Context app — which app the user was in when creating the task
|
|
180
|
+
const contextApp = task.contextApp || task.context_app;
|
|
181
|
+
if (contextApp) {
|
|
182
|
+
lines.push(`**Context App:** ${contextApp}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
179
185
|
const createdAt = task.createdAt || task.created_at;
|
|
180
186
|
lines.push(`**Created:** ${createdAt || 'unknown'}`);
|
|
181
187
|
|