@masslessai/push-todo 3.10.4 → 3.10.6
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 +66 -1
- package/hooks/session-end.js +1 -0
- package/lib/daemon.js +28 -17
- package/lib/fetch.js +1 -0
- package/lib/utils/format.js +6 -0
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -54,7 +54,12 @@ When this command is invoked:
|
|
|
54
54
|
- Follow the [Auto-Resume from Session Transcript](#auto-resume-from-session-transcript) procedure below
|
|
55
55
|
- Only if the session transcript cannot be found should you begin working from scratch
|
|
56
56
|
|
|
57
|
-
9.
|
|
57
|
+
9. **Load task attachments** before starting work:
|
|
58
|
+
- If the task has **screenshot attachments**: Read each image from `~/Library/Mobile Documents/iCloud~ai~massless~push/Documents/Screenshots/<filename>` using the Read tool. These provide essential visual context.
|
|
59
|
+
- If the task has **link attachments**: Use WebFetch to read linked content when relevant to the task.
|
|
60
|
+
- See [Reading Task Attachments](#reading-task-attachments) for full details.
|
|
61
|
+
|
|
62
|
+
10. If no resumable session exists, begin working on the task normally
|
|
58
63
|
|
|
59
64
|
## Review Mode
|
|
60
65
|
|
|
@@ -339,6 +344,66 @@ Do NOT offer to start working on the task — the daemon is already handling it.
|
|
|
339
344
|
If the session file cannot be found (daemon just started, no output yet):
|
|
340
345
|
- Tell the user: "The daemon just started working on this task. Run `/push-todo <number>` again in a minute for a progress update."
|
|
341
346
|
|
|
347
|
+
## Reading Task Attachments
|
|
348
|
+
|
|
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
|
+
|
|
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
|
+
|
|
361
|
+
### Screenshot Attachments
|
|
362
|
+
|
|
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.
|
|
364
|
+
|
|
365
|
+
**When the task output shows an Attachments > Screenshots section:**
|
|
366
|
+
|
|
367
|
+
1. **Get the filename** from the task output (e.g., `ABC123.png`)
|
|
368
|
+
2. **Read the image** directly from the iCloud folder:
|
|
369
|
+
```
|
|
370
|
+
~/Library/Mobile Documents/iCloud~ai~massless~push/Documents/Screenshots/<filename>
|
|
371
|
+
```
|
|
372
|
+
3. **Use the Read tool** to view the image — Claude Code is multimodal and can interpret screenshots
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
```bash
|
|
376
|
+
# The task shows: "1. ABC123.png (1170x2532)"
|
|
377
|
+
# Read it directly:
|
|
378
|
+
```
|
|
379
|
+
Then use the Read tool on:
|
|
380
|
+
`~/Library/Mobile Documents/iCloud~ai~massless~push/Documents/Screenshots/ABC123.png`
|
|
381
|
+
|
|
382
|
+
**Why this matters:** Screenshots often contain the actual UI, error message, design mockup, or reference material the user was looking at when they created the task. The voice transcript alone may say "fix this" — the screenshot shows WHAT to fix.
|
|
383
|
+
|
|
384
|
+
**If the file doesn't exist locally:** iCloud may not have synced it yet. Tell the user: "The screenshot hasn't synced from iCloud yet. Try opening Finder > iCloud Drive > Push > Screenshots to trigger the download."
|
|
385
|
+
|
|
386
|
+
### Link Attachments
|
|
387
|
+
|
|
388
|
+
Links are reference URLs the user shared when creating the task (e.g., from Safari, WeChat, or other apps).
|
|
389
|
+
|
|
390
|
+
**When the task output shows an Attachments > Links section:**
|
|
391
|
+
|
|
392
|
+
1. Links are displayed as clickable markdown: `🔗 [Title](URL)`
|
|
393
|
+
2. **Use WebFetch** to read the linked content when relevant to the task
|
|
394
|
+
|
|
395
|
+
### Programmatic Access (JSON mode)
|
|
396
|
+
|
|
397
|
+
For scripts or when you need raw attachment data:
|
|
398
|
+
```bash
|
|
399
|
+
push-todo <number> --json
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Key JSON fields:
|
|
403
|
+
- `screenshotAttachmentsJson` — JSON string array of `{id, imageFilename, width, height, capturedAt, sourceApp}`
|
|
404
|
+
- `linkAttachmentsJson` — JSON string array of `{id, url, title, createdAt}`
|
|
405
|
+
- `contextApp` — App the user was in when creating the task
|
|
406
|
+
|
|
342
407
|
## CLI Reference
|
|
343
408
|
|
|
344
409
|
The `push-todo` CLI supports these commands:
|
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
|
}
|
|
@@ -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
|
|