@masslessai/push-todo 3.8.1 → 3.8.3
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/daemon.js +121 -8
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -901,7 +901,7 @@ async function markTaskAsCompleted(displayNumber, taskId, comment) {
|
|
|
901
901
|
* Checks for existing branch commits and PRs to avoid redundant re-execution.
|
|
902
902
|
* Returns true if the task was healed (status updated, no re-execution needed).
|
|
903
903
|
*/
|
|
904
|
-
async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
904
|
+
async function autoHealExistingWork(displayNumber, summary, projectPath, taskId) {
|
|
905
905
|
const suffix = getWorktreeSuffix();
|
|
906
906
|
const branch = `push-${displayNumber}-${suffix}`;
|
|
907
907
|
const gitCwd = projectPath || process.cwd();
|
|
@@ -931,7 +931,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
|
931
931
|
let prState = null;
|
|
932
932
|
try {
|
|
933
933
|
const prResult = execSync(
|
|
934
|
-
`gh pr list --head ${branch} --json url,state --jq '.[0]' 2>/dev/null`,
|
|
934
|
+
`gh pr list --head ${branch} --state all --json url,state --jq '.[0]' 2>/dev/null`,
|
|
935
935
|
{ cwd: gitCwd, timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
936
936
|
).toString().trim();
|
|
937
937
|
if (prResult) {
|
|
@@ -950,25 +950,59 @@ async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
|
950
950
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
951
951
|
summary: executionSummary
|
|
952
952
|
});
|
|
953
|
+
|
|
954
|
+
// Auto-complete since PR is already merged
|
|
955
|
+
let status = 'session_finished';
|
|
956
|
+
if (getAutoCompleteEnabled() && taskId) {
|
|
957
|
+
const comment = `Auto-healed: PR already merged. ${prUrl}`;
|
|
958
|
+
const completed = await markTaskAsCompleted(displayNumber, taskId, comment);
|
|
959
|
+
if (completed) {
|
|
960
|
+
log(`Task #${displayNumber}: auto-completed (PR was already merged)`);
|
|
961
|
+
status = 'completed';
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
953
965
|
completedToday.push({
|
|
954
966
|
displayNumber, summary,
|
|
955
967
|
completedAt: new Date().toISOString(),
|
|
956
|
-
duration: 0, status
|
|
968
|
+
duration: 0, status, prUrl
|
|
957
969
|
});
|
|
958
970
|
return true;
|
|
959
971
|
}
|
|
960
972
|
|
|
961
973
|
if (prUrl && prState === 'OPEN') {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
974
|
+
log(`Task #${displayNumber}: PR already open (${prUrl}), attempting merge`);
|
|
975
|
+
|
|
976
|
+
// Try to merge the existing PR
|
|
977
|
+
let merged = false;
|
|
978
|
+
if (getAutoMergeEnabled()) {
|
|
979
|
+
// Clean up worktree first so gh pr merge --delete-branch can delete the local branch
|
|
980
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
981
|
+
merged = mergePRForTask(displayNumber, prUrl, projectPath);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const executionSummary = merged
|
|
985
|
+
? `Auto-healed: previous PR merged. ${prUrl}`
|
|
986
|
+
: `Auto-healed: previous execution completed. PR pending review: ${prUrl}`;
|
|
965
987
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
966
988
|
summary: executionSummary
|
|
967
989
|
});
|
|
990
|
+
|
|
991
|
+
// Auto-complete if merge succeeded
|
|
992
|
+
let status = 'session_finished';
|
|
993
|
+
if (getAutoCompleteEnabled() && merged && taskId) {
|
|
994
|
+
const comment = `Auto-healed and merged. ${prUrl}`;
|
|
995
|
+
const completed = await markTaskAsCompleted(displayNumber, taskId, comment);
|
|
996
|
+
if (completed) {
|
|
997
|
+
log(`Task #${displayNumber}: auto-completed after auto-heal merge`);
|
|
998
|
+
status = 'completed';
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
968
1002
|
completedToday.push({
|
|
969
1003
|
displayNumber, summary,
|
|
970
1004
|
completedAt: new Date().toISOString(),
|
|
971
|
-
duration: 0, status
|
|
1005
|
+
duration: 0, status, prUrl
|
|
972
1006
|
});
|
|
973
1007
|
return true;
|
|
974
1008
|
}
|
|
@@ -1213,7 +1247,8 @@ async function executeTask(task) {
|
|
|
1213
1247
|
}
|
|
1214
1248
|
|
|
1215
1249
|
// Auto-heal: check if previous execution already completed work for this task
|
|
1216
|
-
const
|
|
1250
|
+
const taskId = task.id || task.todo_id || '';
|
|
1251
|
+
const healed = await autoHealExistingWork(displayNumber, summary, projectPath, taskId);
|
|
1217
1252
|
if (healed) {
|
|
1218
1253
|
log(`Task #${displayNumber}: auto-healed from previous execution, skipping re-execution`);
|
|
1219
1254
|
return null;
|
|
@@ -1423,6 +1458,30 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1423
1458
|
merged = mergePRForTask(displayNumber, prUrl, projectPath);
|
|
1424
1459
|
}
|
|
1425
1460
|
|
|
1461
|
+
// Safety net: if no new PR was created (Claude found work already done),
|
|
1462
|
+
// check if a previous PR for this branch was already merged.
|
|
1463
|
+
// See: docs/20260211_auto_complete_failure_investigation.md (Fix D)
|
|
1464
|
+
if (!prUrl && !merged) {
|
|
1465
|
+
const suffix = getWorktreeSuffix();
|
|
1466
|
+
const branch = `push-${displayNumber}-${suffix}`;
|
|
1467
|
+
try {
|
|
1468
|
+
const prCheck = execFileSync('gh', [
|
|
1469
|
+
'pr', 'list', '--head', branch, '--state', 'merged',
|
|
1470
|
+
'--json', 'url', '--jq', '.[0].url'
|
|
1471
|
+
], {
|
|
1472
|
+
cwd: projectPath || process.cwd(),
|
|
1473
|
+
timeout: 15000,
|
|
1474
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1475
|
+
}).toString().trim();
|
|
1476
|
+
if (prCheck) {
|
|
1477
|
+
log(`Task #${displayNumber}: found previously merged PR: ${prCheck}`);
|
|
1478
|
+
merged = true;
|
|
1479
|
+
}
|
|
1480
|
+
} catch {
|
|
1481
|
+
// gh not available or no merged PR found
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1426
1485
|
// Auto-complete task after successful merge (configurable, default ON)
|
|
1427
1486
|
const taskId = info.taskId;
|
|
1428
1487
|
if (getAutoCompleteEnabled() && merged && taskId) {
|
|
@@ -1716,6 +1775,53 @@ async function pollAndExecute() {
|
|
|
1716
1775
|
updateStatusFile();
|
|
1717
1776
|
}
|
|
1718
1777
|
|
|
1778
|
+
/**
|
|
1779
|
+
* Recover tasks orphaned by a previous daemon instance.
|
|
1780
|
+
* When the daemon restarts, tasks claimed by this machine may be stuck in 'running'
|
|
1781
|
+
* with no process working on them. Reset them to 'queued' so the normal poll cycle
|
|
1782
|
+
* picks them up and autoHeal detects prior work.
|
|
1783
|
+
*/
|
|
1784
|
+
async function recoverOrphanedTasks() {
|
|
1785
|
+
const suffix = getWorktreeSuffix();
|
|
1786
|
+
if (!suffix) return;
|
|
1787
|
+
|
|
1788
|
+
try {
|
|
1789
|
+
const params = new URLSearchParams();
|
|
1790
|
+
params.set('execution_status', 'running');
|
|
1791
|
+
|
|
1792
|
+
const response = await apiRequest(`synced-todos?${params}`);
|
|
1793
|
+
if (!response.ok) return;
|
|
1794
|
+
|
|
1795
|
+
const data = await response.json();
|
|
1796
|
+
const tasks = data.todos || [];
|
|
1797
|
+
|
|
1798
|
+
// Filter to tasks owned by THIS machine (branch contains our suffix)
|
|
1799
|
+
const orphaned = tasks.filter(t => {
|
|
1800
|
+
const branch = t.executionBranch || t.execution_branch || '';
|
|
1801
|
+
return branch.endsWith(suffix);
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
if (orphaned.length === 0) return;
|
|
1805
|
+
|
|
1806
|
+
log(`Recovering ${orphaned.length} orphaned task(s) from previous daemon instance`);
|
|
1807
|
+
|
|
1808
|
+
for (const task of orphaned) {
|
|
1809
|
+
const dn = task.displayNumber || task.display_number;
|
|
1810
|
+
log(`Task #${dn}: resetting from 'running' to 'queued' (orphaned by restart)`);
|
|
1811
|
+
await updateTaskStatus(dn, 'queued', {
|
|
1812
|
+
event: {
|
|
1813
|
+
type: 'requeued',
|
|
1814
|
+
timestamp: new Date().toISOString(),
|
|
1815
|
+
machineName: getMachineName() || undefined,
|
|
1816
|
+
summary: 'Daemon restarted — re-queuing for auto-heal'
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
log(`Orphaned task recovery failed (non-fatal): ${error.message}`);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1719
1825
|
async function mainLoop() {
|
|
1720
1826
|
daemonStartTime = new Date().toISOString();
|
|
1721
1827
|
|
|
@@ -1756,6 +1862,13 @@ async function mainLoop() {
|
|
|
1756
1862
|
writeFileSync(VERSION_FILE, getVersion());
|
|
1757
1863
|
} catch {}
|
|
1758
1864
|
|
|
1865
|
+
// Recover orphaned tasks from previous daemon instance
|
|
1866
|
+
// When the daemon restarts (self-update, crash, reboot), tasks may be stuck
|
|
1867
|
+
// in 'running' with no process actually working on them. Reset them to 'queued'
|
|
1868
|
+
// so the normal poll cycle picks them up and autoHeal handles the rest.
|
|
1869
|
+
// See: docs/20260211_auto_complete_failure_investigation.md
|
|
1870
|
+
await recoverOrphanedTasks();
|
|
1871
|
+
|
|
1759
1872
|
// Initial status
|
|
1760
1873
|
updateStatusFile();
|
|
1761
1874
|
|