@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 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. If no resumable session exists, begin working on the task normally
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:
@@ -78,6 +78,7 @@ async function reportSessionFinished(apiKey, activeTask) {
78
78
  : null;
79
79
 
80
80
  const payload = {
81
+ todoId: activeTask.taskId || undefined,
81
82
  displayNumber: activeTask.displayNumber,
82
83
  status: 'session_finished',
83
84
  machineId,
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
- displayNumber,
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: task.id || task.todo_id || '',
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,
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "3.10.4",
3
+ "version": "3.10.6",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {