@masslessai/push-todo 4.0.6 → 4.0.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.
Files changed (3) hide show
  1. package/SKILL.md +15 -2
  2. package/lib/daemon.js +152 -74
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -47,11 +47,24 @@ When this command is invoked:
47
47
  - The task is queued and waiting for the daemon to pick it up
48
48
  - Tell the user: "This task is queued and will be picked up by the daemon shortly."
49
49
  - Do NOT start working on this task
50
+ - For both running and queued tasks, remind the user they can monitor or resume later:
51
+ ```
52
+ To resume the daemon's session later, open a new terminal and run:
53
+
54
+ push-todo --resume <number>
55
+ ```
50
56
 
51
57
  8. **Check for resumable daemon sessions:**
52
58
  - If the task output contains `**Session:** Resumable`, the daemon already ran Claude Code on this task
53
- - Do NOT start working from scratch — automatically load the daemon's session context
54
- - Follow the [Auto-Resume from Session Transcript](#auto-resume-from-session-transcript) procedure below
59
+ - **Tell the user they can resume the daemon's exact Claude session** in a separate terminal:
60
+ ```
61
+ To resume the daemon's Claude session, open a new terminal and run:
62
+
63
+ push-todo --resume <number>
64
+
65
+ This cannot run inside Claude Code (sessions can't nest).
66
+ ```
67
+ - Then follow the [Auto-Resume from Session Transcript](#auto-resume-from-session-transcript) procedure to load the daemon's work context into THIS session
55
68
  - Only if the session transcript cannot be found should you begin working from scratch
56
69
 
57
70
  9. **Load task attachments** before starting work:
package/lib/daemon.js CHANGED
@@ -448,10 +448,23 @@ async function fetchQueuedTasks() {
448
448
 
449
449
  async function fetchScheduledTodos() {
450
450
  try {
451
+ const machineId = getMachineId();
451
452
  const params = new URLSearchParams();
452
453
  params.set('scheduled_before', new Date().toISOString());
454
+ if (machineId) {
455
+ params.set('machine_id', machineId);
456
+ }
453
457
 
454
- const response = await apiRequest(`synced-todos?${params}`);
458
+ // Include registered git_remotes so backend can scope to this machine's projects
459
+ const projects = getListedProjects();
460
+ const gitRemotes = Object.keys(projects);
461
+ const headers = {};
462
+ if (machineId && gitRemotes.length > 0) {
463
+ headers['X-Machine-Id'] = machineId;
464
+ headers['X-Git-Remotes'] = gitRemotes.join(',');
465
+ }
466
+
467
+ const response = await apiRequest(`synced-todos?${params}`, { headers });
455
468
  if (!response.ok) return [];
456
469
 
457
470
  const data = await response.json();
@@ -466,11 +479,33 @@ async function checkAndQueueScheduledTodos() {
466
479
  const scheduledTodos = await fetchScheduledTodos();
467
480
  if (scheduledTodos.length === 0) return;
468
481
 
482
+ const registeredProjects = getListedProjects();
483
+
469
484
  for (const todo of scheduledTodos) {
470
- const dn = todo.displayNumber;
485
+ const dn = todo.displayNumber || todo.display_number;
471
486
  const todoId = todo.id;
472
487
 
473
- log(`Schedule triggered for #${dn} (reminder_date: ${todo.reminderDate})`);
488
+ // Skip tasks already in an execution state (running, queued, completed, failed, etc.)
489
+ const execStatus = todo.executionStatus || todo.execution_status;
490
+ if (execStatus && execStatus !== 'none') {
491
+ continue;
492
+ }
493
+
494
+ // Skip completed tasks
495
+ if (todo.isCompleted || todo.is_completed) {
496
+ continue;
497
+ }
498
+
499
+ // Skip tasks whose project is not registered on this machine
500
+ const gitRemote = todo.gitRemote || todo.git_remote;
501
+ if (gitRemote && Object.keys(registeredProjects).length > 0) {
502
+ if (!(gitRemote in registeredProjects)) {
503
+ log(`Schedule skipped #${dn}: project ${gitRemote} not registered on this machine`);
504
+ continue;
505
+ }
506
+ }
507
+
508
+ log(`Schedule triggered for #${dn} (reminder_date: ${todo.reminderDate || todo.reminder_date})`);
474
509
 
475
510
  try {
476
511
  await updateTaskStatus(dn, 'queued', {
@@ -557,8 +592,7 @@ async function claimTask(displayNumber, todoId, sessionId) {
557
592
  return true;
558
593
  }
559
594
 
560
- const suffix = getWorktreeSuffix();
561
- const branch = `push-${displayNumber}-${suffix}`;
595
+ const branch = getBranchName(displayNumber);
562
596
 
563
597
  const payload = {
564
598
  todoId: todoId || undefined, // UUID lookup (primary, avoids display_number collisions)
@@ -731,62 +765,32 @@ function getWorktreeSuffix() {
731
765
  return 'local';
732
766
  }
733
767
 
734
- function getWorktreePath(displayNumber, projectPath) {
768
+ // Worktree name passed to Claude Code's --worktree flag
769
+ function getWorktreeName(displayNumber) {
735
770
  const suffix = getWorktreeSuffix();
736
- const worktreeName = `push-${displayNumber}-${suffix}`;
737
-
738
- if (projectPath) {
739
- return join(dirname(projectPath), worktreeName);
740
- }
741
- return join(process.cwd(), '..', worktreeName);
771
+ return `push-${displayNumber}-${suffix}`;
742
772
  }
743
773
 
744
- function createWorktree(displayNumber, projectPath) {
745
- const suffix = getWorktreeSuffix();
746
- const branch = `push-${displayNumber}-${suffix}`;
747
- const worktreePath = getWorktreePath(displayNumber, projectPath);
748
-
749
- if (existsSync(worktreePath)) {
750
- log(`Worktree already exists: ${worktreePath}`);
751
- return worktreePath;
752
- }
753
-
754
- const gitCwd = projectPath || process.cwd();
774
+ // Branch name created by Claude Code's --worktree (adds "worktree-" prefix)
775
+ function getBranchName(displayNumber) {
776
+ return `worktree-${getWorktreeName(displayNumber)}`;
777
+ }
755
778
 
756
- try {
757
- // Try to create with new branch
758
- execSync(`git worktree add "${worktreePath}" -b ${branch}`, {
759
- cwd: gitCwd,
760
- timeout: 30000,
761
- stdio: 'pipe'
762
- });
763
- log(`Created worktree: ${worktreePath}`);
764
- return worktreePath;
765
- } catch {
766
- // Branch might already exist, try without -b
767
- try {
768
- execSync(`git worktree add "${worktreePath}" ${branch}`, {
769
- cwd: gitCwd,
770
- timeout: 30000,
771
- stdio: 'pipe'
772
- });
773
- log(`Created worktree (existing branch): ${worktreePath}`);
774
- return worktreePath;
775
- } catch (e) {
776
- logError(`Failed to create worktree: ${e.message}`);
777
- return null;
778
- }
779
- }
779
+ function getWorktreePath(displayNumber, projectPath) {
780
+ const wtName = getWorktreeName(displayNumber);
781
+ const basePath = projectPath || process.cwd();
782
+ return join(basePath, '.claude', 'worktrees', wtName);
780
783
  }
781
784
 
785
+ // createWorktree() removed — Claude Code's --worktree flag handles creation
786
+
782
787
  function cleanupWorktree(displayNumber, projectPath) {
783
788
  const worktreePath = getWorktreePath(displayNumber, projectPath);
784
789
 
785
790
  if (!existsSync(worktreePath)) return;
786
791
 
787
792
  const gitCwd = projectPath || process.cwd();
788
- const suffix = getWorktreeSuffix();
789
- const branch = `push-${displayNumber}-${suffix}`;
793
+ const branch = getBranchName(displayNumber);
790
794
 
791
795
  try {
792
796
  execSync(`git worktree remove "${worktreePath}" --force`, {
@@ -804,8 +808,7 @@ function cleanupWorktree(displayNumber, projectPath) {
804
808
  // ==================== PR Auto-Creation ====================
805
809
 
806
810
  function createPRForTask(displayNumber, summary, projectPath) {
807
- const suffix = getWorktreeSuffix();
808
- const branch = `push-${displayNumber}-${suffix}`;
811
+ const branch = getBranchName(displayNumber);
809
812
  const gitCwd = projectPath || process.cwd();
810
813
 
811
814
  try {
@@ -889,8 +892,7 @@ function mergePRForTask(displayNumber, prUrl, projectPath) {
889
892
  if (firstResult !== 'conflict') return false; // non-conflict failure
890
893
 
891
894
  // Conflict detected — try to resolve with Claude
892
- const suffix = getWorktreeSuffix();
893
- const branch = `push-${displayNumber}-${suffix}`;
895
+ const branch = getBranchName(displayNumber);
894
896
  const resolved = resolveConflictsWithClaude(displayNumber, branch, gitCwd);
895
897
  if (!resolved) return false;
896
898
 
@@ -1103,13 +1105,14 @@ async function hasApprovedConfirmation(displayNumber) {
1103
1105
  * Returns true if the task was healed (status updated, no re-execution needed).
1104
1106
  */
1105
1107
  async function autoHealExistingWork(displayNumber, summary, projectPath, taskId) {
1106
- const suffix = getWorktreeSuffix();
1107
- const branch = `push-${displayNumber}-${suffix}`;
1108
+ const branch = getBranchName(displayNumber);
1109
+ const legacyBranch = `push-${displayNumber}-${getWorktreeSuffix()}`;
1108
1110
  const gitCwd = projectPath || process.cwd();
1109
1111
 
1110
1112
  try {
1111
- // Check if branch has commits ahead of main
1113
+ // Check if branch has commits ahead of main (try new name, then legacy)
1112
1114
  let hasCommits = false;
1115
+ let activeBranch = branch;
1113
1116
  try {
1114
1117
  const logResult = execSync(
1115
1118
  `git log HEAD..origin/${branch} --oneline 2>/dev/null || git log HEAD..${branch} --oneline 2>/dev/null`,
@@ -1117,22 +1120,35 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
1117
1120
  ).toString().trim();
1118
1121
  hasCommits = logResult.length > 0;
1119
1122
  } catch {
1120
- // Branch doesn't exist — no previous work
1121
- return false;
1123
+ // New branch doesn't exist — try legacy branch from pre-migration daemon
1124
+ try {
1125
+ const legacyResult = execSync(
1126
+ `git log HEAD..origin/${legacyBranch} --oneline 2>/dev/null || git log HEAD..${legacyBranch} --oneline 2>/dev/null`,
1127
+ { cwd: gitCwd, timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] }
1128
+ ).toString().trim();
1129
+ if (legacyResult.length > 0) {
1130
+ hasCommits = true;
1131
+ activeBranch = legacyBranch;
1132
+ log(`Task #${displayNumber}: found work on legacy branch ${legacyBranch}`);
1133
+ }
1134
+ } catch {
1135
+ // Neither branch exists — no previous work
1136
+ return false;
1137
+ }
1122
1138
  }
1123
1139
 
1124
1140
  if (!hasCommits) {
1125
1141
  return false;
1126
1142
  }
1127
1143
 
1128
- log(`Task #${displayNumber}: found existing commits on branch ${branch}`);
1144
+ log(`Task #${displayNumber}: found existing commits on branch ${activeBranch}`);
1129
1145
 
1130
1146
  // Check for existing PR
1131
1147
  let prUrl = null;
1132
1148
  let prState = null;
1133
1149
  try {
1134
1150
  const prResult = execSync(
1135
- `gh pr list --head ${branch} --state all --json url,state --jq '.[0]' 2>/dev/null`,
1151
+ `gh pr list --head ${activeBranch} --state all --json url,state --jq '.[0]' 2>/dev/null`,
1136
1152
  { cwd: gitCwd, timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] }
1137
1153
  ).toString().trim();
1138
1154
  if (prResult) {
@@ -1391,6 +1407,51 @@ function extractSemanticSummary(worktreePath, sessionId) {
1391
1407
  }
1392
1408
  }
1393
1409
 
1410
+ /**
1411
+ * Ask Claude to generate a Mermaid diagram of the key change it made.
1412
+ * Uses the same session-resume pattern as extractSemanticSummary.
1413
+ *
1414
+ * @param {string} worktreePath - Path to the git worktree where Claude ran
1415
+ * @param {string} sessionId - Claude session ID
1416
+ * @returns {string|null} Mermaid diagram source code, or null if extraction fails
1417
+ */
1418
+ function extractVisualArtifact(worktreePath, sessionId) {
1419
+ if (!worktreePath || !sessionId) return null;
1420
+
1421
+ try {
1422
+ const result = execFileSync('claude', [
1423
+ '--resume', sessionId,
1424
+ '--print',
1425
+ 'Generate a Mermaid diagram showing the key architectural change or flow you implemented. ' +
1426
+ 'Use graph LR for component relationships, sequenceDiagram for API/data flow, ' +
1427
+ 'or gitGraph for branch operations. Keep it simple (5-10 nodes max). ' +
1428
+ 'Output ONLY the raw Mermaid code. No markdown fences, no explanation, no comments.'
1429
+ ], {
1430
+ cwd: worktreePath,
1431
+ timeout: 30000,
1432
+ stdio: ['ignore', 'pipe', 'pipe']
1433
+ });
1434
+
1435
+ const mermaid = result.toString().trim();
1436
+ if (!mermaid || mermaid.length === 0) return null;
1437
+
1438
+ // Validate: must start with a known Mermaid diagram keyword
1439
+ const validStarts = ['graph ', 'graph\n', 'flowchart ', 'flowchart\n',
1440
+ 'sequenceDiagram', 'gitGraph', 'classDiagram', 'stateDiagram',
1441
+ 'erDiagram', 'gantt', 'pie'];
1442
+ if (!validStarts.some(s => mermaid.startsWith(s))) {
1443
+ log(`Visual artifact extraction returned non-Mermaid content, skipping`);
1444
+ return null;
1445
+ }
1446
+
1447
+ // Cap at 2000 chars — diagrams beyond this are too complex to be useful
1448
+ return mermaid.length > 2000 ? mermaid.slice(0, 2000) : mermaid;
1449
+ } catch (error) {
1450
+ log(`Visual artifact extraction failed: ${error.message}`);
1451
+ return null;
1452
+ }
1453
+ }
1454
+
1394
1455
  // ==================== Task Execution ====================
1395
1456
 
1396
1457
  function updateTaskDetail(displayNumber, updates) {
@@ -1479,13 +1540,9 @@ async function executeTask(task) {
1479
1540
 
1480
1541
  log(`Executing task #${displayNumber}: ${content.slice(0, 60)}...`);
1481
1542
 
1482
- // Create worktree
1483
- const worktreePath = createWorktree(displayNumber, projectPath);
1484
- if (!worktreePath) {
1485
- await updateTaskStatus(displayNumber, 'failed', { error: 'Failed to create git worktree' }, taskId);
1486
- taskDetails.delete(displayNumber);
1487
- return null;
1488
- }
1543
+ // Worktree path — Claude Code's --worktree flag handles creation
1544
+ const worktreeName = getWorktreeName(displayNumber);
1545
+ const worktreePath = getWorktreePath(displayNumber, projectPath);
1489
1546
 
1490
1547
  taskProjectPaths.set(displayNumber, projectPath);
1491
1548
 
@@ -1540,6 +1597,7 @@ async function executeTask(task) {
1540
1597
 
1541
1598
  const claudeArgs = [
1542
1599
  '-p', prompt,
1600
+ '--worktree', worktreeName,
1543
1601
  '--allowedTools', allowedTools,
1544
1602
  '--output-format', 'json',
1545
1603
  '--permission-mode', 'bypassPermissions',
@@ -1548,7 +1606,7 @@ async function executeTask(task) {
1548
1606
 
1549
1607
  try {
1550
1608
  const child = spawn('claude', claudeArgs, {
1551
- cwd: worktreePath,
1609
+ cwd: projectPath || process.cwd(),
1552
1610
  stdio: ['ignore', 'pipe', 'pipe'],
1553
1611
  env: (() => {
1554
1612
  const env = { ...process.env, PUSH_TASK_ID: task.id, PUSH_DISPLAY_NUMBER: String(displayNumber) };
@@ -1675,8 +1733,11 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1675
1733
  // Auto-create PR first so we can include it in the summary
1676
1734
  const prUrl = createPRForTask(displayNumber, summary, projectPath);
1677
1735
 
1678
- // Ask Claude to summarize what it accomplished (needs worktree path)
1679
- const semanticSummary = extractSemanticSummary(worktreePath, sessionId);
1736
+ // Ask Claude to summarize what it accomplished.
1737
+ // If --worktree auto-removed (no code changes), fall back to project path.
1738
+ const summaryPath = existsSync(worktreePath) ? worktreePath : (projectPath || process.cwd());
1739
+ const semanticSummary = extractSemanticSummary(summaryPath, sessionId);
1740
+ const visualArtifact = extractVisualArtifact(summaryPath, sessionId);
1680
1741
 
1681
1742
  // Clean up worktree BEFORE merge — gh pr merge --delete-branch fails if
1682
1743
  // the local branch is still referenced by a worktree. The branch itself
@@ -1706,6 +1767,22 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1706
1767
  }, info.taskId);
1707
1768
  }
1708
1769
 
1770
+ // Append visual artifact as a separate timeline event (non-blocking)
1771
+ if (visualArtifact) {
1772
+ log(`Task #${displayNumber}: Sending visual artifact (${visualArtifact.length} chars)`);
1773
+ await updateTaskStatus(displayNumber, 'session_finished', {
1774
+ event: {
1775
+ type: 'visual_artifact',
1776
+ timestamp: new Date().toISOString(),
1777
+ machineName: machineName || undefined,
1778
+ format: 'mermaid',
1779
+ content: visualArtifact
1780
+ }
1781
+ }, info.taskId).catch(err => {
1782
+ log(`Task #${displayNumber}: Visual artifact upload failed (non-fatal): ${err.message}`);
1783
+ });
1784
+ }
1785
+
1709
1786
  if (NOTIFY_ON_COMPLETE) {
1710
1787
  const prNote = prUrl ? ' PR ready for review.' : '';
1711
1788
  sendMacNotification(
@@ -1725,8 +1802,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1725
1802
  // check if a previous PR for this branch was already merged.
1726
1803
  // See: docs/20260211_auto_complete_failure_investigation.md (Fix D)
1727
1804
  if (!prUrl && !merged) {
1728
- const suffix = getWorktreeSuffix();
1729
- const branch = `push-${displayNumber}-${suffix}`;
1805
+ const branch = getBranchName(displayNumber);
1730
1806
  try {
1731
1807
  const prCheck = execFileSync('gh', [
1732
1808
  'pr', 'list', '--head', branch, '--state', 'merged',
@@ -1794,8 +1870,10 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1794
1870
  log(`Task #${displayNumber} stderr: ${stderr.slice(0, 500)}`);
1795
1871
  }
1796
1872
 
1797
- // Ask Claude to explain what went wrong (needs worktree path)
1798
- const failureSummary = extractSemanticSummary(worktreePath, sessionId);
1873
+ // Ask Claude to explain what went wrong.
1874
+ // If --worktree auto-removed, fall back to project path.
1875
+ const failSummaryPath = existsSync(worktreePath) ? worktreePath : (projectPath || process.cwd());
1876
+ const failureSummary = extractSemanticSummary(failSummaryPath, sessionId);
1799
1877
 
1800
1878
  // Clean up worktree after summary extraction
1801
1879
  cleanupWorktree(displayNumber, projectPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.0.6",
3
+ "version": "4.0.9",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {