@yemi33/minions 0.1.57 → 0.1.59

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/engine.js CHANGED
@@ -102,7 +102,7 @@ function log(level, msg, meta = {}) {
102
102
  let logData = safeJson(LOG_PATH) || [];
103
103
  if (!Array.isArray(logData)) logData = logData.entries || [];
104
104
  logData.push(entry);
105
- if (logData.length > 500) logData.splice(0, logData.length - 500);
105
+ if (logData.length > 2000) logData.splice(0, logData.length - 2000);
106
106
  safeWrite(LOG_PATH, logData);
107
107
  }
108
108
 
@@ -158,7 +158,7 @@ function parseRoutingTable() {
158
158
 
159
159
  function getRoutingTableCached() {
160
160
  let mtime = 0;
161
- try { mtime = fs.statSync(ROUTING_PATH).mtimeMs; } catch {}
161
+ try { mtime = fs.statSync(ROUTING_PATH).mtimeMs; } catch { /* optional */ }
162
162
  if (_routingCache && _routingCacheMtime === mtime) return _routingCache;
163
163
  _routingCache = parseRoutingTable();
164
164
  _routingCacheMtime = mtime;
@@ -287,7 +287,7 @@ function resolveTaskContext(item, config) {
287
287
  resolved.additionalContext += `\n\n## Referenced Plan: ${planFile} (created by ${agent.name})\n\n${content}`;
288
288
  resolved.referencedFiles.push(planPath);
289
289
  log('info', `Context resolution: found plan "${planFile}" by ${agent.name} for work item ${item.id}`);
290
- } catch {}
290
+ } catch (e) { log('warn', 'resolve plan context: ' + e.message); }
291
291
  } else if (plans.length > 0) {
292
292
  // Fallback: try to find a plan file with the agent's name or ID in it
293
293
  const match = plans.find(f => f.toLowerCase().includes(agent.id) || f.toLowerCase().includes(agent.name));
@@ -298,10 +298,10 @@ function resolveTaskContext(item, config) {
298
298
  resolved.additionalContext += `\n\n## Referenced Plan: ${match}\n\n${content}`;
299
299
  resolved.referencedFiles.push(planPath);
300
300
  log('info', `Context resolution: found plan "${match}" (name match) for work item ${item.id}`);
301
- } catch {}
301
+ } catch (e) { log('warn', 'resolve plan fallback context: ' + e.message); }
302
302
  }
303
303
  }
304
- } catch {}
304
+ } catch (e) { log('warn', 'resolve agent plan context: ' + e.message); }
305
305
  }
306
306
 
307
307
  // Match agent output/notes references
@@ -322,7 +322,7 @@ function resolveTaskContext(item, config) {
322
322
  resolved.referencedFiles.push(path.join(inboxDir, files[0]));
323
323
  log('info', `Context resolution: found notes "${files[0]}" by ${agent.name} for work item ${item.id}`);
324
324
  }
325
- } catch {}
325
+ } catch (e) { log('warn', 'resolve plan context outer: ' + e.message); }
326
326
  }
327
327
  }
328
328
 
@@ -340,7 +340,7 @@ function resolveTaskContext(item, config) {
340
340
  resolved.referencedFiles.push(planPath);
341
341
  log('info', `Context resolution: using latest plan "${plans[0]}" for work item ${item.id}`);
342
342
  }
343
- } catch {}
343
+ } catch (e) { log('warn', 'resolve latest plan context: ' + e.message); }
344
344
  }
345
345
 
346
346
  return resolved;
@@ -358,7 +358,7 @@ function renderPlaybook(type, vars) {
358
358
 
359
359
  // Inject pinned context (always visible to agents) — capped at 4KB
360
360
  let pinnedContent = '';
361
- try { pinnedContent = fs.readFileSync(path.join(MINIONS_DIR, 'pinned.md'), 'utf8'); } catch {}
361
+ try { pinnedContent = fs.readFileSync(path.join(MINIONS_DIR, 'pinned.md'), 'utf8'); } catch { /* optional */ }
362
362
  if (pinnedContent) {
363
363
  if (pinnedContent.length > 4096) pinnedContent = pinnedContent.slice(0, 4096) + '\n\n_...pinned.md truncated (read full file if needed)_';
364
364
  content += '\n\n---\n\n## Pinned Context (CRITICAL — READ FIRST)\n\n' + pinnedContent;
@@ -612,7 +612,7 @@ function buildAgentContext(agentId, config, project) {
612
612
  for (const pr of centralPrs.filter(pr => pr.status === 'active' || pr.status === 'linked')) {
613
613
  if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
614
614
  }
615
- } catch {}
615
+ } catch (e) { log('warn', 'read central pull-requests: ' + e.message); }
616
616
  if (allPrs.length > 0) {
617
617
  const prLines = allPrs.map(pr =>
618
618
  `- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${pr.status === 'linked' ? 'context-only' : (pr.reviewStatus || 'pending')}${pr.buildStatus === 'failing' ? ', BUILD FAILING' : ''}]${pr.branch ? ' branch: `' + pr.branch + '`' : ''}${pr._context ? ' — ' + pr._context.slice(0, 100) : ''}`
@@ -703,7 +703,7 @@ function findExistingWorktree(repoDir, branchName) {
703
703
  }
704
704
  }
705
705
  }
706
- } catch {}
706
+ } catch (e) { log('warn', 'git: ' + e.message); }
707
707
  return null;
708
708
  }
709
709
 
@@ -727,7 +727,7 @@ function removeStaleIndexLock(rootDir) {
727
727
  log('warn', `Removed stale index.lock (${Math.round(age / 1000)}s old) in ${rootDir}`);
728
728
  }
729
729
  }
730
- } catch {}
730
+ } catch (e) { log('warn', 'git: ' + e.message); }
731
731
  }
732
732
 
733
733
  function runWorktreeAdd(rootDir, worktreePath, args, gitOpts, worktreeCreateRetries) {
@@ -736,7 +736,7 @@ function runWorktreeAdd(rootDir, worktreePath, args, gitOpts, worktreeCreateRetr
736
736
  for (let attempt = 0; attempt <= retries; attempt++) {
737
737
  try {
738
738
  if (attempt > 0) {
739
- try { exec('git worktree prune', { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch {}
739
+ try { exec('git worktree prune', { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch (e) { log('warn', 'git: ' + e.message); }
740
740
  removeStaleIndexLock(rootDir);
741
741
  log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
742
742
  }
@@ -795,8 +795,8 @@ function spawnAgent(dispatchItem, config) {
795
795
  if (existingWt) {
796
796
  worktreePath = existingWt;
797
797
  log('info', `Reusing existing worktree for ${branchName}: ${existingWt}`);
798
- try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
799
- try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: existingWt }); } catch {}
798
+ try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
799
+ try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: existingWt }); } catch (e) { log('warn', 'git: ' + e.message); }
800
800
  } else if (type !== 'implement') {
801
801
  // Only implement tasks may create new worktrees.
802
802
  // Other task types are reuse-only: if no existing worktree, run in rootDir.
@@ -808,13 +808,13 @@ function spawnAgent(dispatchItem, config) {
808
808
  if (!fs.existsSync(worktreePath)) {
809
809
  const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
810
810
  // Prune stale worktree entries before creating (handles leftover entries from crashed runs)
811
- try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
811
+ try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
812
812
  // Remove stale index.lock before creating worktree (Windows crashes can leave this behind)
813
813
  removeStaleIndexLock(rootDir);
814
814
 
815
815
  if (isSharedBranch) {
816
816
  log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
817
- try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
817
+ try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
818
818
  try {
819
819
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
820
820
  } catch (eShared) {
@@ -832,7 +832,7 @@ function spawnAgent(dispatchItem, config) {
832
832
  runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${sanitizeBranch(project.mainBranch || 'main')}`, _worktreeGitOpts, worktreeCreateRetries);
833
833
  } catch (e1) {
834
834
  // Branch already exists or checked out elsewhere — try without -b
835
- try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch {}
835
+ try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
836
836
  try {
837
837
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
838
838
  log('info', `Reusing existing branch: ${branchName}`);
@@ -859,12 +859,12 @@ function spawnAgent(dispatchItem, config) {
859
859
  } else if (existingWtPath && !fs.existsSync(existingWtPath)) {
860
860
  // Directory gone but git still tracks it — prune and recreate
861
861
  log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
862
- try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
862
+ try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
863
863
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
864
864
  log('info', `Recovered worktree for ${branchName} after stale entry prune`);
865
865
  } else {
866
866
  // Can't find the worktree at all — prune and retry
867
- try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch {}
867
+ try { exec(`git worktree prune`, { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
868
868
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
869
869
  }
870
870
  } else {
@@ -875,7 +875,7 @@ function spawnAgent(dispatchItem, config) {
875
875
  }
876
876
  } else if (meta?.branchStrategy === 'shared-branch') {
877
877
  log('info', `Pulling latest on shared branch ${branchName}`);
878
- try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: worktreePath }); } catch {}
878
+ try { exec(`git pull origin "${branchName}"`, { ..._gitOpts, cwd: worktreePath }); } catch (e) { log('warn', 'git: ' + e.message); }
879
879
  }
880
880
  } catch (err) {
881
881
  if (recoverPartialWorktree(rootDir, worktreePath, branchName, _gitOpts)) {
@@ -956,7 +956,7 @@ function spawnAgent(dispatchItem, config) {
956
956
  log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
957
957
  }
958
958
  }
959
- } catch {}
959
+ } catch (e) { log('warn', 'session resume lookup: ' + e.message); }
960
960
  }
961
961
 
962
962
  // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
@@ -997,14 +997,14 @@ function spawnAgent(dispatchItem, config) {
997
997
  const silentMs = Date.now() - lastOutputAt;
998
998
  if (silentMs < 30000) return;
999
999
  const silentSec = Math.round(silentMs / 1000);
1000
- try { fs.appendFileSync(liveOutputPath, `[heartbeat] running — no output for ${silentSec}s\n`); } catch {}
1000
+ try { fs.appendFileSync(liveOutputPath, `[heartbeat] running — no output for ${silentSec}s\n`); } catch { /* optional */ }
1001
1001
  }, 30000);
1002
1002
 
1003
1003
  proc.stdout.on('data', (data) => {
1004
1004
  const chunk = data.toString();
1005
1005
  lastOutputAt = Date.now();
1006
1006
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1007
- try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
1007
+ try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1008
1008
 
1009
1009
  // Capture sessionId early for mid-session steering
1010
1010
  const procInfo = activeProcesses.get(id);
@@ -1021,7 +1021,7 @@ function spawnAgent(dispatchItem, config) {
1021
1021
  break;
1022
1022
  }
1023
1023
  }
1024
- } catch {}
1024
+ } catch { /* JSON parse — output may not be valid JSON */ }
1025
1025
  }
1026
1026
  });
1027
1027
 
@@ -1029,7 +1029,7 @@ function spawnAgent(dispatchItem, config) {
1029
1029
  const chunk = data.toString();
1030
1030
  lastOutputAt = Date.now();
1031
1031
  if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
1032
- try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
1032
+ try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
1033
1033
  });
1034
1034
 
1035
1035
  function onAgentClose(code) {
@@ -1077,13 +1077,13 @@ function spawnAgent(dispatchItem, config) {
1077
1077
  const chunk = data.toString();
1078
1078
  lastOutputAt = Date.now();
1079
1079
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1080
- try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
1080
+ try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1081
1081
  });
1082
1082
  resumeProc.stderr.on('data', (data) => {
1083
1083
  const chunk = data.toString();
1084
1084
  lastOutputAt = Date.now();
1085
1085
  if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
1086
- try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
1086
+ try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
1087
1087
  });
1088
1088
 
1089
1089
  // Re-wire close handler for the resumed process
@@ -1106,9 +1106,9 @@ function spawnAgent(dispatchItem, config) {
1106
1106
  const stillActive = (dispatchNow.active || []).some(d => d.id === id);
1107
1107
  if (!stillActive) {
1108
1108
  log('info', `Agent ${agentId} (${id}) close event ignored — dispatch already completed elsewhere`);
1109
- try { fs.unlinkSync(sysPromptPath); } catch {}
1110
- try { fs.unlinkSync(promptPath); } catch {}
1111
- try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
1109
+ try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
1110
+ try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
1111
+ try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
1112
1112
  return;
1113
1113
  }
1114
1114
 
@@ -1124,9 +1124,9 @@ function spawnAgent(dispatchItem, config) {
1124
1124
  const errMsg = stderr.includes('claude-code') ? stderr.trim() : 'Configuration error — Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code';
1125
1125
  log('error', `Agent ${agentId} (${id}) failed: ${errMsg}`);
1126
1126
  completeDispatch(id, 'error', errMsg, '');
1127
- try { fs.unlinkSync(sysPromptPath); } catch {}
1128
- try { fs.unlinkSync(promptPath); } catch {}
1129
- try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
1127
+ try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
1128
+ try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
1129
+ try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
1130
1130
  return;
1131
1131
  }
1132
1132
 
@@ -1137,9 +1137,9 @@ function spawnAgent(dispatchItem, config) {
1137
1137
  completeDispatch(id, code === 0 ? 'success' : 'error', '', resultSummary);
1138
1138
 
1139
1139
  // Cleanup temp files (including PID file now that dispatch is complete)
1140
- try { fs.unlinkSync(sysPromptPath); } catch {}
1141
- try { fs.unlinkSync(promptPath); } catch {}
1142
- try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
1140
+ try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
1141
+ try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
1142
+ try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
1143
1143
 
1144
1144
  log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
1145
1145
 
@@ -1151,7 +1151,7 @@ function spawnAgent(dispatchItem, config) {
1151
1151
  // Keep output archive but remove temp agent directory (live-output.log etc.)
1152
1152
  fs.rmSync(agentDir, { recursive: true, force: true });
1153
1153
  log('info', `Temp agent ${agentId} cleaned up`);
1154
- } catch {}
1154
+ } catch { /* cleanup */ }
1155
1155
  }
1156
1156
  }
1157
1157
 
@@ -1287,7 +1287,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
1287
1287
  const wi = items.find(i => i.id === item.meta.item.id);
1288
1288
  if (wi) retries = wi._retryCount || 0;
1289
1289
  }
1290
- } catch {}
1290
+ } catch (e) { log('warn', 'read retry count: ' + e.message); }
1291
1291
  if (retryableFailure && retries < 3) {
1292
1292
  log('info', `Dispatch error for ${item.meta.item.id} — auto-retry ${retries + 1}/3`);
1293
1293
  updateWorkItemStatus(item.meta, 'pending', '');
@@ -1298,7 +1298,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
1298
1298
  dp.completed = Array.isArray(dp.completed) ? dp.completed.filter(d => d.meta?.dispatchKey !== item.meta.dispatchKey) : [];
1299
1299
  return dp;
1300
1300
  });
1301
- } catch {}
1301
+ } catch (e) { log('warn', 'clear dispatch for retry: ' + e.message); }
1302
1302
  }
1303
1303
  // Increment retry counter on the source work item
1304
1304
  try {
@@ -1320,7 +1320,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
1320
1320
  safeWrite(wiPath, items);
1321
1321
  }
1322
1322
  }
1323
- } catch {}
1323
+ } catch (e) { log('warn', 'increment retry counter: ' + e.message); }
1324
1324
  } else {
1325
1325
  const finalReason = !retryableFailure
1326
1326
  ? `Non-retryable failure: ${reason || 'Unknown error'}`
@@ -1349,7 +1349,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
1349
1349
  `These items cannot dispatch until \`${failedId}\` is fixed and reset to \`pending\`.\n`
1350
1350
  : `No downstream items are blocked.\n`)
1351
1351
  );
1352
- } catch {}
1352
+ } catch (e) { log('warn', 'write failure alert: ' + e.message); }
1353
1353
  }
1354
1354
  }
1355
1355
  }
@@ -1370,7 +1370,7 @@ function areDependenciesMet(item, config) {
1370
1370
  try {
1371
1371
  const wi = safeJson(projectWorkItemsPath(p)) || [];
1372
1372
  allWorkItems = allWorkItems.concat(wi);
1373
- } catch {}
1373
+ } catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
1374
1374
  }
1375
1375
  // PRD item statuses that count as "done" for dep resolution
1376
1376
  const PRD_MET_STATUSES = new Set(['done', 'in-pr', 'implemented', 'complete']);
@@ -1383,7 +1383,7 @@ function areDependenciesMet(item, config) {
1383
1383
  const plan = safeJson(path.join(PRD_DIR, sourcePlan));
1384
1384
  const prdItem = (plan?.missing_features || []).find(f => f.id === depId);
1385
1385
  if (prdItem && PRD_MET_STATUSES.has(prdItem.status)) continue; // PRD says done — treat as met
1386
- } catch {}
1386
+ } catch (e) { log('warn', 'check PRD dep status: ' + e.message); }
1387
1387
  log('warn', `Dependency ${depId} not found for ${item.id} (plan: ${sourcePlan}) — treating as unmet`);
1388
1388
  return false;
1389
1389
  }
@@ -1419,7 +1419,7 @@ function writeInboxAlert(slug, content) {
1419
1419
  const existing = safeReadDir(INBOX_DIR).find(f => f.startsWith(`engine-alert-${slug}-${dateStamp()}`));
1420
1420
  if (existing) return;
1421
1421
  safeWrite(file, content);
1422
- } catch {}
1422
+ } catch (e) { log('warn', 'write inbox alert: ' + e.message); }
1423
1423
  }
1424
1424
 
1425
1425
  // Reconciles work items against known PRs.
@@ -1526,7 +1526,7 @@ function checkSteering(config) {
1526
1526
  if (!fs.existsSync(steerPath)) continue;
1527
1527
 
1528
1528
  const message = safeRead(steerPath);
1529
- try { fs.unlinkSync(steerPath); } catch {}
1529
+ try { fs.unlinkSync(steerPath); } catch { /* cleanup */ }
1530
1530
  if (!message) continue;
1531
1531
 
1532
1532
  const sessionId = info.sessionId;
@@ -1538,7 +1538,7 @@ function checkSteering(config) {
1538
1538
  log('info', `Steering: killing ${info.agentId} (${id}) for session resume with human message`);
1539
1539
 
1540
1540
  // Kill current process
1541
- try { info.proc.kill('SIGTERM'); } catch {}
1541
+ try { info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
1542
1542
 
1543
1543
  // Store steering context for re-spawn on close
1544
1544
  info._steeringMessage = message;
@@ -1558,9 +1558,9 @@ function checkTimeouts(config) {
1558
1558
  const elapsed = Date.now() - new Date(info.startedAt).getTime();
1559
1559
  if (elapsed > itemTimeout) {
1560
1560
  log('warn', `Agent ${info.agentId} (${id}) hit hard timeout after ${Math.round(elapsed / 1000)}s — killing`);
1561
- try { info.proc.kill('SIGTERM'); } catch {}
1561
+ try { info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
1562
1562
  setTimeout(() => {
1563
- try { info.proc.kill('SIGKILL'); } catch {}
1563
+ try { info.proc.kill('SIGKILL'); } catch { /* process may be dead */ }
1564
1564
  }, 5000);
1565
1565
  }
1566
1566
  }
@@ -1581,7 +1581,7 @@ function checkTimeouts(config) {
1581
1581
  try {
1582
1582
  const stat = fs.statSync(liveLogPath);
1583
1583
  lastActivity = Math.max(lastActivity, stat.mtimeMs);
1584
- } catch {}
1584
+ } catch { /* optional */ }
1585
1585
 
1586
1586
  const silentMs = Date.now() - lastActivity;
1587
1587
  const silentSec = Math.round(silentMs / 1000);
@@ -1605,7 +1605,7 @@ function checkTimeouts(config) {
1605
1605
  const result = JSON.parse(resultLine);
1606
1606
  safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${isSuccess ? 0 : 1}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${result.result || '(no text)'}\n`);
1607
1607
  }
1608
- } catch {}
1608
+ } catch (e) { log('warn', 'parse output result: ' + e.message); }
1609
1609
 
1610
1610
  completeDispatch(item.id, isSuccess ? 'success' : 'error', 'Completed (detected from output)');
1611
1611
 
@@ -1613,12 +1613,12 @@ function checkTimeouts(config) {
1613
1613
  runPostCompletionHooks(item, item.agent, isSuccess ? 0 : 1, liveLog, config);
1614
1614
 
1615
1615
  if (hasProcess) {
1616
- try { activeProcesses.get(item.id)?.proc.kill('SIGTERM'); } catch {}
1616
+ try { activeProcesses.get(item.id)?.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
1617
1617
  activeProcesses.delete(item.id);
1618
1618
  }
1619
1619
  continue; // Skip orphan/hung detection — we handled it
1620
1620
  }
1621
- } catch {}
1621
+ } catch (e) { log('warn', 'output completion detection: ' + e.message); }
1622
1622
 
1623
1623
  // Check if agent is in a blocking tool call (TaskOutput block:true, Bash with long timeout, etc.)
1624
1624
  // These tools produce no stdout for extended periods — don't kill them prematurely
@@ -1652,13 +1652,13 @@ function checkTimeouts(config) {
1652
1652
  isBlocking = true;
1653
1653
  }
1654
1654
  break; // only check the most recent tool_use
1655
- } catch {}
1655
+ } catch { /* JSON parse — line may not be valid JSON */ }
1656
1656
  }
1657
1657
  if (isBlocking) {
1658
1658
  log('info', `Agent ${item.agent} (${item.id}) is in a blocking tool call — extended timeout to ${Math.round(blockingTimeout / 1000)}s (silent for ${silentSec}s)`);
1659
1659
  }
1660
1660
  }
1661
- } catch {}
1661
+ } catch (e) { log('warn', 'blocking tool detection: ' + e.message); }
1662
1662
  }
1663
1663
 
1664
1664
  const effectiveTimeout = isBlocking ? blockingTimeout : heartbeatTimeout;
@@ -1672,8 +1672,8 @@ function checkTimeouts(config) {
1672
1672
  log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
1673
1673
  const procInfo = activeProcesses.get(item.id);
1674
1674
  if (procInfo) {
1675
- try { procInfo.proc.kill('SIGTERM'); } catch {}
1676
- setTimeout(() => { try { procInfo.proc.kill('SIGKILL'); } catch {} }, 5000);
1675
+ try { procInfo.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
1676
+ setTimeout(() => { try { procInfo.proc.kill('SIGKILL'); } catch { /* process may be dead */ } }, 5000);
1677
1677
  activeProcesses.delete(item.id);
1678
1678
  }
1679
1679
  deadItems.push({ item, reason: `Hung — no output for ${silentSec}s` });
@@ -1752,11 +1752,11 @@ function runCleanup(config, verbose = false) {
1752
1752
  fs.unlinkSync(fp);
1753
1753
  cleaned.tempFiles++;
1754
1754
  }
1755
- } catch {}
1755
+ } catch { /* cleanup */ }
1756
1756
  }
1757
1757
  }
1758
1758
  }
1759
- } catch {}
1759
+ } catch (e) { log('warn', 'cleanup temp files: ' + e.message); }
1760
1760
 
1761
1761
  // 2. Clean live-output.log for idle agents (not currently working)
1762
1762
  for (const [agentId] of Object.entries(config.agents || {})) {
@@ -1770,7 +1770,7 @@ function runCleanup(config, verbose = false) {
1770
1770
  fs.unlinkSync(livePath);
1771
1771
  cleaned.liveOutputs++;
1772
1772
  }
1773
- } catch {}
1773
+ } catch { /* cleanup */ }
1774
1774
  }
1775
1775
  }
1776
1776
  }
@@ -1835,7 +1835,7 @@ function runCleanup(config, verbose = false) {
1835
1835
  if (ageMs > 7200000 && !isReferenced) { // 2 hours
1836
1836
  shouldClean = true;
1837
1837
  }
1838
- } catch {}
1838
+ } catch { /* optional */ }
1839
1839
  }
1840
1840
 
1841
1841
  // Skip worktrees for active shared-branch plans (check both prd/ and plans/ for .json PRDs)
@@ -1859,7 +1859,7 @@ function runCleanup(config, verbose = false) {
1859
1859
  }
1860
1860
  if (isProtected) break;
1861
1861
  }
1862
- } catch {}
1862
+ } catch (e) { log('warn', 'check shared-branch protection: ' + e.message); }
1863
1863
  }
1864
1864
 
1865
1865
  wtEntries.push({ dir, wtPath, mtime, shouldClean, isProtected });
@@ -1889,7 +1889,7 @@ function runCleanup(config, verbose = false) {
1889
1889
  }
1890
1890
  }
1891
1891
  }
1892
- } catch {}
1892
+ } catch (e) { log('warn', 'cleanup worktrees: ' + e.message); }
1893
1893
  }
1894
1894
 
1895
1895
  // 4. Kill zombie claude processes not tracked by the engine
@@ -1905,15 +1905,15 @@ function runCleanup(config, verbose = false) {
1905
1905
  const activeIds = new Set((dispatch.active || []).map(d => d.id));
1906
1906
  for (const [id, info] of activeProcesses.entries()) {
1907
1907
  if (!activeIds.has(id)) {
1908
- try { if (info.proc) info.proc.kill('SIGTERM'); } catch {}
1908
+ try { if (info.proc) info.proc.kill('SIGTERM'); } catch { /* process may be dead */ }
1909
1909
  activeProcesses.delete(id);
1910
1910
  cleaned.zombies++;
1911
1911
  }
1912
1912
  }
1913
- } catch {}
1913
+ } catch (e) { log('warn', 'cleanup zombie processes: ' + e.message); }
1914
1914
 
1915
1915
  // 5. Clean spawn-debug.log
1916
- try { fs.unlinkSync(path.join(ENGINE_DIR, 'spawn-debug.log')); } catch {}
1916
+ try { fs.unlinkSync(path.join(ENGINE_DIR, 'spawn-debug.log')); } catch { /* cleanup */ }
1917
1917
 
1918
1918
  // 6. Prune old output archive files (keep last 30 per agent)
1919
1919
  for (const agentId of Object.keys(config.agents || {})) {
@@ -1925,9 +1925,9 @@ function runCleanup(config, verbose = false) {
1925
1925
  .map(f => ({ name: f, mtime: fs.statSync(path.join(agentDir, f)).mtimeMs }))
1926
1926
  .sort((a, b) => b.mtime - a.mtime);
1927
1927
  for (const old of outputFiles.slice(30)) {
1928
- try { fs.unlinkSync(path.join(agentDir, old.name)); cleaned.files++; } catch {}
1928
+ try { fs.unlinkSync(path.join(agentDir, old.name)); cleaned.files++; } catch { /* cleanup */ }
1929
1929
  }
1930
- } catch {}
1930
+ } catch (e) { log('warn', 'prune output archives: ' + e.message); }
1931
1931
  }
1932
1932
 
1933
1933
  // 7. Prune orphaned dispatch entries — items whose source work item no longer exists
@@ -1939,12 +1939,12 @@ function runCleanup(config, verbose = false) {
1939
1939
  try {
1940
1940
  const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
1941
1941
  central.forEach(w => allWiIds.add(w.id));
1942
- } catch {}
1942
+ } catch (e) { log('warn', 'read central work items for orphan check: ' + e.message); }
1943
1943
  for (const project of projects) {
1944
1944
  try {
1945
1945
  const projItems = safeJson(projectWorkItemsPath(project)) || [];
1946
1946
  projItems.forEach(w => allWiIds.add(w.id));
1947
- } catch {}
1947
+ } catch (e) { log('warn', 'read project work items for orphan check: ' + e.message); }
1948
1948
  }
1949
1949
 
1950
1950
  let changed = false;
@@ -1974,7 +1974,7 @@ function runCleanup(config, verbose = false) {
1974
1974
  }
1975
1975
  });
1976
1976
  }
1977
- } catch {}
1977
+ } catch (e) { log('warn', 'prune orphaned dispatches: ' + e.message); }
1978
1978
 
1979
1979
  if (cleaned.tempFiles + cleaned.liveOutputs + cleaned.worktrees + cleaned.zombies + (cleaned.files || 0) + cleaned.orphanedDispatches > 0) {
1980
1980
  log('info', `Cleanup: ${cleaned.tempFiles} temp, ${cleaned.liveOutputs} live outputs, ${cleaned.worktrees} worktrees, ${cleaned.zombies} zombies, ${cleaned.files || 0} archives, ${cleaned.orphanedDispatches} orphaned dispatches`);
@@ -1993,10 +1993,10 @@ function runCleanup(config, verbose = false) {
1993
1993
  if (!cleaned.sweptKb) cleaned.sweptKb = 0;
1994
1994
  cleaned.sweptKb++;
1995
1995
  }
1996
- } catch {}
1996
+ } catch { /* cleanup */ }
1997
1997
  }
1998
1998
  }
1999
- } catch {}
1999
+ } catch (e) { log('warn', 'cleanup swept KB files: ' + e.message); }
2000
2000
 
2001
2001
  // 9. KB watchdog — restore deleted KB files from git if count dropped vs checkpoint
2002
2002
  try {
@@ -2024,7 +2024,7 @@ function runCleanup(config, verbose = false) {
2024
2024
  }
2025
2025
  }
2026
2026
  }
2027
- } catch {}
2027
+ } catch (e) { log('warn', 'KB watchdog check: ' + e.message); }
2028
2028
 
2029
2029
  // 6. Migrate legacy work-item statuses to canonical values
2030
2030
  // in-pr, implemented, complete → done (one-time correction per item)
@@ -2044,7 +2044,7 @@ function runCleanup(config, verbose = false) {
2044
2044
  safeWrite(wiPath, items);
2045
2045
  log('info', `Migrated ${migrated} legacy status(es) → done in ${project.name} work items`);
2046
2046
  }
2047
- } catch {}
2047
+ } catch (e) { log('warn', 'migrate legacy statuses: ' + e.message); }
2048
2048
  }
2049
2049
  // Central work items
2050
2050
  try {
@@ -2061,7 +2061,7 @@ function runCleanup(config, verbose = false) {
2061
2061
  safeWrite(centralPath, centralItems);
2062
2062
  log('info', `Migrated ${migrated} legacy status(es) → done in central work items`);
2063
2063
  }
2064
- } catch {}
2064
+ } catch (e) { log('warn', 'migrate central legacy statuses: ' + e.message); }
2065
2065
  // PRD items (missing_features[].status)
2066
2066
  try {
2067
2067
  const prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
@@ -2081,7 +2081,7 @@ function runCleanup(config, verbose = false) {
2081
2081
  log('info', `Migrated ${migrated} legacy PRD item status(es) → done in ${pf}`);
2082
2082
  }
2083
2083
  }
2084
- } catch {}
2084
+ } catch (e) { log('warn', 'migrate PRD legacy statuses: ' + e.message); }
2085
2085
 
2086
2086
  return cleaned;
2087
2087
  }
@@ -2204,7 +2204,7 @@ function autoCleanPrdWorkItems(prdFile, config) {
2204
2204
  return true;
2205
2205
  });
2206
2206
  if (filtered.length < items.length) safeWrite(wiPath, filtered);
2207
- } catch {}
2207
+ } catch (e) { log('warn', 'auto-clean PRD work items: ' + e.message); }
2208
2208
  }
2209
2209
  if (deletedIds.length > 0) {
2210
2210
  const deletedSet = new Set(deletedIds);
@@ -2214,7 +2214,7 @@ function autoCleanPrdWorkItems(prdFile, config) {
2214
2214
  }
2215
2215
 
2216
2216
  function materializePlansAsWorkItems(config) {
2217
- if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch {} }
2217
+ if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
2218
2218
 
2219
2219
  // Enforce: PRDs must be .json — auto-rename .md files that contain valid PRD JSON
2220
2220
  // Check both prd/ and plans/ (agents may still write JSON to plans/)
@@ -2231,12 +2231,12 @@ function materializePlansAsWorkItems(config) {
2231
2231
  if (parsed.missing_features) {
2232
2232
  const jsonName = mf.replace(/\.md$/, '.json');
2233
2233
  safeWrite(path.join(PRD_DIR, jsonName), parsed);
2234
- try { fs.unlinkSync(path.join(checkDir, mf)); } catch {}
2234
+ try { fs.unlinkSync(path.join(checkDir, mf)); } catch { /* cleanup */ }
2235
2235
  log('info', `Plan enforcement: moved ${mf} → prd/${jsonName} (PRDs must be .json in prd/)`);
2236
2236
  }
2237
2237
  } catch {} // Not JSON — it's a proper plan .md, leave it
2238
2238
  }
2239
- } catch {}
2239
+ } catch (e) { log('warn', 'scan .md files for PRD enforcement: ' + e.message); }
2240
2240
  // Also migrate any .json PRD files from plans/ to prd/
2241
2241
  if (checkDir === PLANS_DIR) {
2242
2242
  try {
@@ -2246,12 +2246,12 @@ function materializePlansAsWorkItems(config) {
2246
2246
  const parsed = safeJson(path.join(PLANS_DIR, jf));
2247
2247
  if (parsed?.missing_features) {
2248
2248
  safeWrite(path.join(PRD_DIR, jf), parsed);
2249
- try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch {}
2249
+ try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch { /* cleanup */ }
2250
2250
  log('info', `Auto-migrated PRD ${jf} from plans/ to prd/`);
2251
2251
  }
2252
- } catch {}
2252
+ } catch (e) { log('warn', 'migrate PRD from plans: ' + e.message); }
2253
2253
  }
2254
- } catch {}
2254
+ } catch (e) { log('warn', 'scan JSON in plans dir: ' + e.message); }
2255
2255
  }
2256
2256
  }
2257
2257
 
@@ -2303,7 +2303,7 @@ function materializePlansAsWorkItems(config) {
2303
2303
  : '';
2304
2304
 
2305
2305
  // Delete old PRD — agent will write replacement at same path
2306
- try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch {}
2306
+ try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch { /* cleanup */ }
2307
2307
 
2308
2308
  // Queue plan-to-prd regeneration
2309
2309
  const planContent = safeRead(path.join(PLANS_DIR, plan.source_plan));
@@ -2341,7 +2341,7 @@ function materializePlansAsWorkItems(config) {
2341
2341
 
2342
2342
  safeWrite(path.join(PRD_DIR, file), plan);
2343
2343
  }
2344
- } catch {}
2344
+ } catch (e) { log('warn', 'plan staleness check: ' + e.message); }
2345
2345
  }
2346
2346
 
2347
2347
  // Human approval gate: plans start as 'awaiting-approval' and must be approved before work begins
@@ -2561,7 +2561,7 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
2561
2561
  if (!target?.humanFeedback?.pendingFix) return;
2562
2562
  target.humanFeedback.pendingFix = false;
2563
2563
  safeWrite(prsPath, prs);
2564
- } catch {}
2564
+ } catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
2565
2565
  }
2566
2566
 
2567
2567
  /**
@@ -2681,7 +2681,7 @@ function discoverFromPrs(config, project) {
2681
2681
  target._buildFailNotified = true;
2682
2682
  safeWrite(prPath, prs);
2683
2683
  }
2684
- } catch {}
2684
+ } catch (e) { log('warn', 'mark build fail notified: ' + e.message); }
2685
2685
  }
2686
2686
  }
2687
2687
 
@@ -2747,7 +2747,7 @@ function discoverFromWorkItems(config, project) {
2747
2747
  return dp.completed.length !== before ? dp : undefined;
2748
2748
  });
2749
2749
  dispatchCooldowns.delete(key);
2750
- } catch {}
2750
+ } catch (e) { log('warn', 'self-heal dispatch state: ' + e.message); }
2751
2751
  // Cooldown bypass for resumed items — clear in-memory cooldown so they dispatch immediately
2752
2752
  if (item._resumedAt) {
2753
2753
  dispatchCooldowns.delete(key);
@@ -2809,7 +2809,7 @@ function discoverFromWorkItems(config, project) {
2809
2809
  commit_message: item.commitMessage || `feat: ${item.title || item.id}`,
2810
2810
  notes_content: '',
2811
2811
  };
2812
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
2812
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
2813
2813
 
2814
2814
  // Inject references and acceptance criteria
2815
2815
  const refs = (item.references || []).filter(r => r && r.url).map(r =>
@@ -2824,7 +2824,7 @@ function discoverFromWorkItems(config, project) {
2824
2824
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
2825
2825
  vars.task_id = item.id;
2826
2826
  vars.notes_content = '';
2827
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
2827
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
2828
2828
  }
2829
2829
 
2830
2830
  // Resolve implicit context references (e.g., "ripley's plan", "the latest plan")
@@ -2954,7 +2954,7 @@ function materializeSpecsAsWorkItems(config, project) {
2954
2954
  recentSpecs.push({ file: line.trim(), ...currentCommit });
2955
2955
  }
2956
2956
  }
2957
- } catch {}
2957
+ } catch (e) { log('warn', 'git: ' + e.message); }
2958
2958
  }
2959
2959
 
2960
2960
  if (recentSpecs.length === 0) return;
@@ -3118,7 +3118,7 @@ function discoverCentralWorkItems(config) {
3118
3118
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
3119
3119
  vars.task_id = item.id;
3120
3120
  vars.notes_content = '';
3121
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
3121
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
3122
3122
  }
3123
3123
 
3124
3124
  const resolvedCtx = resolveTaskContext(item, config);
@@ -3179,7 +3179,7 @@ function discoverCentralWorkItems(config) {
3179
3179
  project_path: firstProject?.localPath || '',
3180
3180
  notes_content: '',
3181
3181
  };
3182
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
3182
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
3183
3183
 
3184
3184
  // Inject references and acceptance criteria
3185
3185
  const normRefs = (item.references || []).filter(r => r && r.url).map(r =>
@@ -3199,7 +3199,7 @@ function discoverCentralWorkItems(config) {
3199
3199
  vars.plan_file = planFileName;
3200
3200
  vars.task_description = item.title;
3201
3201
  vars.notes_content = '';
3202
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
3202
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
3203
3203
  // Track expected plan filename in meta for chainPlanToPrd
3204
3204
  item._planFileName = planFileName;
3205
3205
  }
@@ -3228,7 +3228,7 @@ function discoverCentralWorkItems(config) {
3228
3228
  vars.question = item.title + (item.description ? '\n\n' + item.description : '');
3229
3229
  vars.task_id = item.id;
3230
3230
  vars.notes_content = '';
3231
- try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch {}
3231
+ try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
3232
3232
  }
3233
3233
 
3234
3234
  // Resolve implicit context references
@@ -3321,7 +3321,7 @@ function discoverWork(config) {
3321
3321
  }
3322
3322
  if (added > 0) safeWrite(centralPath, items);
3323
3323
  }
3324
- } catch {}
3324
+ } catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
3325
3325
 
3326
3326
  // Gate reviews and fixes: do not dispatch until all implement items are complete
3327
3327
  const hasIncompleteImplements = projects.some(project => {
@@ -3381,7 +3381,7 @@ async function tickInner() {
3381
3381
  }
3382
3382
 
3383
3383
  // Write heartbeat so dashboard can detect stale engine
3384
- try { safeWrite(CONTROL_PATH, { ...control, heartbeat: Date.now() }); } catch {}
3384
+ try { safeWrite(CONTROL_PATH, { ...control, heartbeat: Date.now() }); } catch (e) { log('warn', 'write heartbeat: ' + e.message); }
3385
3385
 
3386
3386
  const config = getConfig();
3387
3387
  tickCount++;
@@ -3480,7 +3480,7 @@ async function tickInner() {
3480
3480
  dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
3481
3481
  if (dp.completed.length !== before) return dp;
3482
3482
  });
3483
- } catch {}
3483
+ } catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
3484
3484
 
3485
3485
  // Clear cooldown so item isn't blocked by exponential backoff
3486
3486
  try {
@@ -3489,7 +3489,7 @@ async function tickInner() {
3489
3489
  dispatchCooldowns.delete(key);
3490
3490
  saveCooldowns();
3491
3491
  }
3492
- } catch {}
3492
+ } catch (e) { log('warn', 'stall recovery clear cooldown: ' + e.message); }
3493
3493
  }
3494
3494
  }
3495
3495
 
@@ -3513,14 +3513,14 @@ async function tickInner() {
3513
3513
  mutateDispatch((dp) => {
3514
3514
  dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
3515
3515
  });
3516
- } catch {}
3516
+ } catch (e) { log('warn', 'stall recovery clear dependent dispatch: ' + e.message); }
3517
3517
  }
3518
3518
  }
3519
3519
  }
3520
3520
  }
3521
3521
 
3522
3522
  if (changed) safeWrite(wiPath, items);
3523
- } catch {}
3523
+ } catch (e) { log('warn', 'stall recovery process project: ' + e.message); }
3524
3524
  }
3525
3525
  }
3526
3526
  } catch (err) { log('warn', `Stall detection error: ${err?.message || err}`); }