@masslessai/push-todo 4.2.4 → 4.2.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/lib/agent-versions.js +70 -1
- package/lib/daemon.js +56 -1
- package/lib/launchagent.js +1 -1
- package/package.json +1 -1
package/lib/agent-versions.js
CHANGED
|
@@ -279,6 +279,8 @@ export function checkForAgentUpdate(agentType) {
|
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
281
|
* Install a specific version of an agent CLI globally.
|
|
282
|
+
* For claude-code: tries npm first, falls back to `claude update` which
|
|
283
|
+
* handles non-npm installations (brew, app installer, happy-coder wrapper).
|
|
282
284
|
*
|
|
283
285
|
* @param {string} agentType
|
|
284
286
|
* @param {string} targetVersion
|
|
@@ -288,6 +290,7 @@ export function performAgentUpdate(agentType, targetVersion) {
|
|
|
288
290
|
const agent = AGENTS[agentType];
|
|
289
291
|
if (!agent?.npmPackage) return false;
|
|
290
292
|
|
|
293
|
+
// Try npm install first (works for standard npm global installs)
|
|
291
294
|
try {
|
|
292
295
|
execFileSync('npm', ['install', '-g', `${agent.npmPackage}@${targetVersion}`], {
|
|
293
296
|
timeout: 120000,
|
|
@@ -295,8 +298,29 @@ export function performAgentUpdate(agentType, targetVersion) {
|
|
|
295
298
|
});
|
|
296
299
|
return true;
|
|
297
300
|
} catch {
|
|
298
|
-
|
|
301
|
+
// npm failed — fall through to agent-specific fallbacks
|
|
299
302
|
}
|
|
303
|
+
|
|
304
|
+
// Fallback: use the agent's own update command (handles non-npm installs)
|
|
305
|
+
if (agentType === 'claude-code') {
|
|
306
|
+
try {
|
|
307
|
+
const env = { ...process.env };
|
|
308
|
+
delete env.CLAUDECODE;
|
|
309
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
310
|
+
execFileSync('claude', ['update'], {
|
|
311
|
+
timeout: 120000,
|
|
312
|
+
stdio: 'pipe',
|
|
313
|
+
env,
|
|
314
|
+
});
|
|
315
|
+
// Verify the update actually worked
|
|
316
|
+
const after = detectAgentVersion('claude-code');
|
|
317
|
+
return after.installed && after.version != null;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
300
324
|
}
|
|
301
325
|
|
|
302
326
|
/**
|
|
@@ -333,6 +357,51 @@ export function checkAllAgentUpdates({ force = false } = {}) {
|
|
|
333
357
|
return results;
|
|
334
358
|
}
|
|
335
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Pre-flight check: verify an agent meets minimum version before task execution.
|
|
362
|
+
* If below minimum, attempts an immediate update and rechecks.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} agentType
|
|
365
|
+
* @returns {{ ok: boolean, version: string|null, error: string|null }}
|
|
366
|
+
*/
|
|
367
|
+
export function ensureAgentReady(agentType) {
|
|
368
|
+
const agent = AGENTS[agentType];
|
|
369
|
+
if (!agent) {
|
|
370
|
+
return { ok: false, version: null, error: `Unknown agent type: ${agentType}` };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const info = detectAgentVersion(agentType);
|
|
374
|
+
if (!info.installed) {
|
|
375
|
+
return { ok: false, version: null, error: `${agentType} CLI not found` };
|
|
376
|
+
}
|
|
377
|
+
if (!info.version) {
|
|
378
|
+
return { ok: false, version: null, error: `${agentType} installed but version unknown` };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check minimum version
|
|
382
|
+
if (agent.minVersion && compareSemver(info.version, agent.minVersion) < 0) {
|
|
383
|
+
// Attempt immediate update
|
|
384
|
+
const latest = fetchLatestAgentVersion(agentType);
|
|
385
|
+
if (latest?.version) {
|
|
386
|
+
const updated = performAgentUpdate(agentType, latest.version);
|
|
387
|
+
if (updated) {
|
|
388
|
+
// Recheck after update
|
|
389
|
+
const after = detectAgentVersion(agentType);
|
|
390
|
+
if (after.version && compareSemver(after.version, agent.minVersion) >= 0) {
|
|
391
|
+
return { ok: true, version: after.version, error: null };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
version: info.version,
|
|
398
|
+
error: `${agentType} v${info.version} is below minimum v${agent.minVersion} (needs --worktree support). Update with: claude update`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { ok: true, version: info.version, error: null };
|
|
403
|
+
}
|
|
404
|
+
|
|
336
405
|
// ==================== Version Parity ====================
|
|
337
406
|
|
|
338
407
|
/**
|
package/lib/daemon.js
CHANGED
|
@@ -25,7 +25,7 @@ import { getProjectContext, buildSmartPrompt, invalidateCache } from './context-
|
|
|
25
25
|
import { sendMacNotification } from './utils/notify.js';
|
|
26
26
|
import { checkAndRunDueJobs } from './cron.js';
|
|
27
27
|
import { runHeartbeatChecks } from './heartbeat.js';
|
|
28
|
-
import { getAgentVersions, formatAgentVersionSummary, checkAllAgentUpdates, performAgentUpdate, checkVersionParity } from './agent-versions.js';
|
|
28
|
+
import { getAgentVersions, formatAgentVersionSummary, checkAllAgentUpdates, performAgentUpdate, checkVersionParity, ensureAgentReady } from './agent-versions.js';
|
|
29
29
|
import { checkAllProjectsFreshness } from './project-freshness.js';
|
|
30
30
|
import { getStatus as getLaunchAgentStatus, install as refreshLaunchAgent } from './launchagent.js';
|
|
31
31
|
|
|
@@ -91,6 +91,7 @@ const LOG_FILE = join(PUSH_DIR, 'daemon.log');
|
|
|
91
91
|
const STATUS_FILE = join(PUSH_DIR, 'daemon_status.json');
|
|
92
92
|
const VERSION_FILE = join(PUSH_DIR, 'daemon.version');
|
|
93
93
|
const LOCK_FILE = join(PUSH_DIR, 'daemon.lock');
|
|
94
|
+
const COMPLETED_FILE = join(PUSH_DIR, 'completed_tasks.json');
|
|
94
95
|
const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
95
96
|
const MACHINE_ID_FILE = join(CONFIG_DIR, 'machine_id');
|
|
96
97
|
const REGISTRY_FILE = join(CONFIG_DIR, 'projects.json');
|
|
@@ -110,6 +111,25 @@ function trackCompleted(entry) {
|
|
|
110
111
|
if (completedToday.length > COMPLETED_TODAY_MAX) {
|
|
111
112
|
completedToday.splice(0, completedToday.length - COMPLETED_TODAY_MAX);
|
|
112
113
|
}
|
|
114
|
+
// Persist to disk so new daemon instances skip already-completed tasks
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(COMPLETED_FILE, JSON.stringify(completedToday));
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadCompletedTasks() {
|
|
121
|
+
try {
|
|
122
|
+
if (!existsSync(COMPLETED_FILE)) return;
|
|
123
|
+
const data = JSON.parse(readFileSync(COMPLETED_FILE, 'utf8'));
|
|
124
|
+
if (!Array.isArray(data)) return;
|
|
125
|
+
// Only load entries from the last 24 hours
|
|
126
|
+
const cutoff = Date.now() - 86400000;
|
|
127
|
+
for (const entry of data) {
|
|
128
|
+
if (entry.completedAt && new Date(entry.completedAt).getTime() > cutoff) {
|
|
129
|
+
completedToday.push(entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
113
133
|
}
|
|
114
134
|
const taskLastOutput = new Map(); // displayNumber -> timestamp
|
|
115
135
|
const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
@@ -886,6 +906,16 @@ function createPRForTask(displayNumber, summary, projectPath) {
|
|
|
886
906
|
const gitCwd = projectPath || process.cwd();
|
|
887
907
|
|
|
888
908
|
try {
|
|
909
|
+
// Verify branch exists before comparing (worktree may have been auto-cleaned)
|
|
910
|
+
try {
|
|
911
|
+
execFileSync('git', ['rev-parse', '--verify', branch], {
|
|
912
|
+
cwd: gitCwd, timeout: 5000, stdio: 'pipe'
|
|
913
|
+
});
|
|
914
|
+
} catch {
|
|
915
|
+
log(`Branch ${branch} does not exist, skipping PR creation`);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
|
|
889
919
|
// Check if branch has commits
|
|
890
920
|
const logResult = execSync(`git log HEAD..${branch} --oneline`, {
|
|
891
921
|
cwd: gitCwd,
|
|
@@ -1667,6 +1697,7 @@ function respawnWithInjectedMessage(displayNumber) {
|
|
|
1667
1697
|
'--continue', sessionId,
|
|
1668
1698
|
'-p', injectionPrompt,
|
|
1669
1699
|
'--verbose',
|
|
1700
|
+
'--worktree', getWorktreeName(displayNumber),
|
|
1670
1701
|
'--allowedTools', allowedTools,
|
|
1671
1702
|
'--output-format', 'stream-json',
|
|
1672
1703
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -2230,6 +2261,17 @@ async function executeTask(task) {
|
|
|
2230
2261
|
log(`Task #${displayNumber}: Project ${gitRemote} -> ${projectPath}`);
|
|
2231
2262
|
}
|
|
2232
2263
|
|
|
2264
|
+
// Pre-flight: verify agent CLI meets minimum version (attempts auto-update if not)
|
|
2265
|
+
const agentType = taskActionType || 'claude-code';
|
|
2266
|
+
const readiness = ensureAgentReady(agentType);
|
|
2267
|
+
if (!readiness.ok) {
|
|
2268
|
+
logError(`Task #${displayNumber}: ${readiness.error}`);
|
|
2269
|
+
await updateTaskStatus(displayNumber, 'failed', {
|
|
2270
|
+
error: readiness.error
|
|
2271
|
+
}, taskId);
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2233
2275
|
// Pre-assign session ID so we can store it at claim time (not rely on parsing stdout)
|
|
2234
2276
|
const preAssignedSessionId = randomUUID();
|
|
2235
2277
|
|
|
@@ -2332,6 +2374,7 @@ async function executeTask(task) {
|
|
|
2332
2374
|
'--continue', previousSessionId,
|
|
2333
2375
|
'-p', prompt,
|
|
2334
2376
|
'--verbose',
|
|
2377
|
+
'--worktree', worktreeName,
|
|
2335
2378
|
'--allowedTools', allowedTools,
|
|
2336
2379
|
'--output-format', 'stream-json',
|
|
2337
2380
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -2340,6 +2383,7 @@ async function executeTask(task) {
|
|
|
2340
2383
|
: [
|
|
2341
2384
|
'-p', prompt,
|
|
2342
2385
|
'--verbose',
|
|
2386
|
+
'--worktree', worktreeName,
|
|
2343
2387
|
'--allowedTools', allowedTools,
|
|
2344
2388
|
'--output-format', 'stream-json',
|
|
2345
2389
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -3200,6 +3244,14 @@ async function recoverOrphanedTasks() {
|
|
|
3200
3244
|
for (const task of orphaned) {
|
|
3201
3245
|
const dn = task.displayNumber || task.display_number;
|
|
3202
3246
|
const tid = task.id || task.todo_id || null;
|
|
3247
|
+
|
|
3248
|
+
// Skip tasks already completed by the previous daemon instance
|
|
3249
|
+
// (race condition: API may return stale 'running' status after session_finished update)
|
|
3250
|
+
if (completedToday.some(c => c.displayNumber === dn)) {
|
|
3251
|
+
log(`Task #${dn}: skipping orphan recovery — already completed by previous daemon`);
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3203
3255
|
log(`Task #${dn}: resetting from 'running' to 'queued' (orphaned by restart)`);
|
|
3204
3256
|
await updateTaskStatus(dn, 'queued', {
|
|
3205
3257
|
event: {
|
|
@@ -3259,6 +3311,9 @@ async function mainLoop() {
|
|
|
3259
3311
|
writeFileSync(VERSION_FILE, getVersion());
|
|
3260
3312
|
} catch {}
|
|
3261
3313
|
|
|
3314
|
+
// Load completed tasks from previous daemon instance (prevents re-execution)
|
|
3315
|
+
loadCompletedTasks();
|
|
3316
|
+
|
|
3262
3317
|
// Recover orphaned tasks from previous daemon instance
|
|
3263
3318
|
// When the daemon restarts (self-update, crash, reboot), tasks may be stuck
|
|
3264
3319
|
// in 'running' with no process actually working on them. Reset them to 'queued'
|
package/lib/launchagent.js
CHANGED
|
@@ -68,7 +68,7 @@ function generatePlist() {
|
|
|
68
68
|
<key>PUSH_DAEMON</key>
|
|
69
69
|
<string>1</string>
|
|
70
70
|
<key>PATH</key>
|
|
71
|
-
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${dirname(nodeBin)}</string>
|
|
71
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${dirname(nodeBin)}:${join(homedir(), '.local', 'bin')}</string>
|
|
72
72
|
</dict>
|
|
73
73
|
|
|
74
74
|
<key>RunAtLoad</key>
|