cleanupStaleAgentWorktrees method
Remove stale agent/workflow worktrees older than cutoffDate.
Safety:
- Only touches slugs matching ephemeral patterns (never user-named worktrees)
- Skips the current session's worktree
- Fail-closed: skips if git status fails or shows tracked changes
- Fail-closed: skips if any commits aren't reachable from a remote
Implementation
Future<int> cleanupStaleAgentWorktrees(DateTime cutoffDate) async {
final gitRoot = _findCanonicalGitRoot(_getCwd());
if (gitRoot == null) return 0;
final dir = _worktreesDir(gitRoot);
List<FileSystemEntity> entries;
try {
entries = await Directory(dir).list().toList();
} catch (_) {
return 0;
}
final cutoffMs = cutoffDate.millisecondsSinceEpoch;
final currentPath = currentSession.value?.worktreePath;
int removed = 0;
for (final entry in entries) {
final slug = entry.path.split('/').last;
if (!_ephemeralWorktreePatterns.any((p) => p.hasMatch(slug))) continue;
final worktreePath = '$dir/$slug';
if (currentPath == worktreePath) continue;
int mtimeMs;
try {
final stat = await FileStat.stat(worktreePath);
mtimeMs = stat.modified.millisecondsSinceEpoch;
} catch (_) {
continue;
}
if (mtimeMs >= cutoffMs) continue;
// Both checks must succeed with empty output
final statusFuture = _execGit([
'--no-optional-locks',
'status',
'--porcelain',
'-uno',
], cwd: worktreePath);
final unpushedFuture = _execGit([
'rev-list',
'--max-count=1',
'HEAD',
'--not',
'--remotes',
], cwd: worktreePath);
final results = await Future.wait([statusFuture, unpushedFuture]);
final status = results[0];
final unpushed = results[1];
if (status.code != 0 || status.stdout.trim().isNotEmpty) continue;
if (unpushed.code != 0 || unpushed.stdout.trim().isNotEmpty) continue;
final success = await removeAgentWorktree(
worktreePath,
worktreeBranch: worktreeBranchName(slug),
gitRoot: gitRoot,
);
if (success) removed++;
}
if (removed > 0) {
await _execGit(['worktree', 'prune'], cwd: gitRoot);
_logForDebugging(
'cleanupStaleAgentWorktrees: removed $removed stale worktree(s)',
);
}
return removed;
}