@imdeadpool/guardex 6.0.1 → 7.0.0

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.
@@ -5,7 +5,6 @@ BASE_BRANCH="${GUARDEX_BASE_BRANCH:-}"
5
5
  BASE_BRANCH_EXPLICIT=0
6
6
  DRY_RUN=0
7
7
  FORCE_DIRTY=0
8
- FORCE_MERGED=0
9
8
  DELETE_BRANCHES=0
10
9
  DELETE_REMOTE_BRANCHES=0
11
10
  ONLY_DIRTY_WORKTREES=0
@@ -34,10 +33,6 @@ while [[ $# -gt 0 ]]; do
34
33
  FORCE_DIRTY=1
35
34
  shift
36
35
  ;;
37
- --force-merged)
38
- FORCE_MERGED=1
39
- shift
40
- ;;
41
36
  --delete-branches)
42
37
  DELETE_BRANCHES=1
43
38
  shift
@@ -60,7 +55,7 @@ while [[ $# -gt 0 ]]; do
60
55
  ;;
61
56
  *)
62
57
  echo "[agent-worktree-prune] Unknown argument: $1" >&2
63
- echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--force-merged] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...|__agent_integrate_*|__source-probe-*>]" >&2
58
+ echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
64
59
  exit 1
65
60
  ;;
66
61
  esac
@@ -71,15 +66,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
71
66
  exit 1
72
67
  fi
73
68
 
74
- current_worktree_root="$(git rev-parse --show-toplevel)"
75
- common_git_dir_raw="$(git -C "$current_worktree_root" rev-parse --git-common-dir)"
76
- if [[ "$common_git_dir_raw" == /* ]]; then
77
- repo_common_dir="$common_git_dir_raw"
78
- else
79
- repo_common_dir="${current_worktree_root}/${common_git_dir_raw}"
80
- fi
81
- repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
82
- repo_root="$(cd "$repo_common_dir/.." && pwd -P)"
69
+ repo_root="$(git rev-parse --show-toplevel)"
83
70
  current_pwd="$(pwd -P)"
84
71
  worktree_root="${repo_root}/.omx/agent-worktrees"
85
72
  repo_common_dir="$(
@@ -114,28 +101,13 @@ resolve_base_branch() {
114
101
  printf '%s' ""
115
102
  }
116
103
 
117
- is_agent_branch() {
118
- local branch="$1"
119
- [[ "$branch" == agent/* ]]
120
- }
121
-
122
- is_temporary_branch() {
123
- local branch="$1"
124
- [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]
125
- }
126
-
127
- is_supported_target_branch() {
128
- local branch="$1"
129
- is_agent_branch "$branch" || is_temporary_branch "$branch"
130
- }
131
-
132
104
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
133
105
  echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
134
106
  exit 1
135
107
  fi
136
108
 
137
- if [[ -n "$TARGET_BRANCH" ]] && ! is_supported_target_branch "$TARGET_BRANCH"; then
138
- echo "[agent-worktree-prune] --branch must reference agent/*, __agent_integrate_*, or __source-probe-*: ${TARGET_BRANCH}" >&2
109
+ if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
110
+ echo "[agent-worktree-prune] --branch must reference an agent/* branch: ${TARGET_BRANCH}" >&2
139
111
  exit 1
140
112
  fi
141
113
 
@@ -180,10 +152,7 @@ run_cmd() {
180
152
 
181
153
  branch_has_worktree() {
182
154
  local branch="$1"
183
- git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
184
- $1 == "branch" && $2 == target { found = 1; exit }
185
- END { exit(found ? 0 : 1) }
186
- '
155
+ git -C "$repo_root" worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
187
156
  }
188
157
 
189
158
  is_clean_worktree() {
@@ -193,166 +162,6 @@ is_clean_worktree() {
193
162
  && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
194
163
  }
195
164
 
196
- has_unmerged_conflicts() {
197
- local wt="$1"
198
- [[ -n "$(git -C "$wt" diff --name-only --diff-filter=U 2>/dev/null || true)" ]]
199
- }
200
-
201
- filtered_status_output() {
202
- local wt="$1"
203
- # Use --untracked-files=all so untracked paths are reported as individual
204
- # files (not collapsed directories). The bootstrap manifest stores file
205
- # paths, so dir-level entries would never match.
206
- git -C "$wt" status --porcelain --untracked-files=all -- \
207
- . \
208
- ":(exclude).omx/state/agent-file-locks.json" \
209
- ":(exclude).dev-ports.json" \
210
- ":(exclude)apps/logs/*.log"
211
- }
212
-
213
- resolve_worktree_git_dir() {
214
- local wt="$1"
215
- local git_dir=""
216
- git_dir="$(git -C "$wt" rev-parse --git-dir 2>/dev/null || true)"
217
- if [[ -z "$git_dir" ]]; then
218
- return 1
219
- fi
220
- if [[ "$git_dir" == /* ]]; then
221
- git_dir="$(cd "$git_dir" 2>/dev/null && pwd -P || true)"
222
- else
223
- git_dir="$(cd "$wt/$git_dir" 2>/dev/null && pwd -P || true)"
224
- fi
225
- if [[ -z "$git_dir" ]]; then
226
- return 1
227
- fi
228
- printf '%s' "$git_dir"
229
- }
230
-
231
- bootstrap_manifest_path_for_worktree() {
232
- local wt="$1"
233
- local git_dir=""
234
- git_dir="$(resolve_worktree_git_dir "$wt" || true)"
235
- if [[ -z "$git_dir" ]]; then
236
- return 1
237
- fi
238
- printf '%s/guardex-bootstrap-manifest.json' "$git_dir"
239
- }
240
-
241
- worktree_matches_bootstrap_manifest() {
242
- local wt="$1"
243
- local manifest_path=""
244
- local status_output=""
245
-
246
- manifest_path="$(bootstrap_manifest_path_for_worktree "$wt" || true)"
247
- if [[ -z "$manifest_path" || ! -f "$manifest_path" ]]; then
248
- return 1
249
- fi
250
-
251
- status_output="$(filtered_status_output "$wt")"
252
- if [[ -z "$status_output" ]]; then
253
- return 1
254
- fi
255
-
256
- STATUS_OUTPUT="$status_output" python3 - "$wt" "$manifest_path" <<'PY'
257
- from __future__ import annotations
258
-
259
- import hashlib
260
- import json
261
- import os
262
- import sys
263
- from pathlib import Path
264
-
265
-
266
- def parse_status_paths(raw: str) -> list[str]:
267
- paths: list[str] = []
268
- for line in raw.splitlines():
269
- if len(line) < 4:
270
- continue
271
- path_part = line[3:]
272
- if " -> " in path_part:
273
- path_part = path_part.split(" -> ", 1)[1]
274
- path_part = path_part.strip()
275
- if path_part:
276
- paths.append(path_part)
277
- return paths
278
-
279
-
280
- def sha256_for_path(path: Path) -> str | None:
281
- if not path.exists() or not path.is_file():
282
- return None
283
- digest = hashlib.sha256()
284
- with path.open("rb") as handle:
285
- for chunk in iter(lambda: handle.read(65536), b""):
286
- digest.update(chunk)
287
- return digest.hexdigest()
288
-
289
-
290
- if len(sys.argv) != 3:
291
- sys.exit(1)
292
-
293
- worktree_root = Path(sys.argv[1])
294
- manifest_path = Path(sys.argv[2])
295
- status_raw = os.environ.get("STATUS_OUTPUT", "")
296
- status_paths = sorted(set(parse_status_paths(status_raw)))
297
- if not status_paths:
298
- sys.exit(1)
299
-
300
- try:
301
- payload = json.loads(manifest_path.read_text(encoding="utf-8"))
302
- except Exception:
303
- sys.exit(1)
304
-
305
- entries = payload.get("files")
306
- if not isinstance(entries, list):
307
- sys.exit(1)
308
-
309
- manifest_by_path: dict[str, str | None] = {}
310
- for entry in entries:
311
- if not isinstance(entry, dict):
312
- continue
313
- path_value = entry.get("path")
314
- if not isinstance(path_value, str) or not path_value:
315
- continue
316
- sha_value = entry.get("sha256")
317
- if sha_value is not None and not isinstance(sha_value, str):
318
- continue
319
- manifest_by_path[path_value] = sha_value
320
-
321
- if not manifest_by_path:
322
- sys.exit(1)
323
-
324
- for rel_path in status_paths:
325
- if rel_path not in manifest_by_path:
326
- sys.exit(1)
327
- file_path = worktree_root / rel_path
328
- current_sha = sha256_for_path(file_path)
329
- if current_sha != manifest_by_path.get(rel_path):
330
- sys.exit(1)
331
-
332
- sys.exit(0)
333
- PY
334
- }
335
-
336
- sanitize_branch_component() {
337
- local raw="$1"
338
- raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
339
- if [[ -z "$raw" ]]; then
340
- raw="sandbox"
341
- fi
342
- printf '%s' "$raw"
343
- }
344
-
345
- resolve_unique_recovery_branch_name() {
346
- local seed="$1"
347
- local candidate="$seed"
348
- local suffix=2
349
- while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${candidate}"; do
350
- candidate="${seed}-${suffix}"
351
- suffix=$((suffix + 1))
352
- done
353
- printf '%s' "$candidate"
354
- }
355
-
356
165
  resolve_worktree_common_dir() {
357
166
  local wt="$1"
358
167
  local common_dir=""
@@ -383,45 +192,68 @@ select_unique_worktree_path() {
383
192
  printf '%s' "$candidate"
384
193
  }
385
194
 
195
+ read_branch_activity_epoch() {
196
+ local branch="$1"
197
+ local wt="${2:-}"
198
+ local activity_epoch=""
199
+
200
+ activity_epoch="$(
201
+ git -C "$repo_root" reflog show --format='%ct' -n 1 "refs/heads/${branch}" 2>/dev/null \
202
+ | head -n 1 \
203
+ | tr -d '[:space:]'
204
+ )"
205
+ if [[ -z "$activity_epoch" ]]; then
206
+ activity_epoch="$(
207
+ git -C "$repo_root" log -1 --format='%ct' "$branch" 2>/dev/null \
208
+ | head -n 1 \
209
+ | tr -d '[:space:]'
210
+ )"
211
+ fi
212
+
213
+ if [[ -n "$wt" && -d "$wt" ]]; then
214
+ local lock_file="${wt}/.omx/state/agent-file-locks.json"
215
+ if [[ -f "$lock_file" ]]; then
216
+ local lock_mtime=""
217
+ lock_mtime="$(stat -c %Y "$lock_file" 2>/dev/null || stat -f %m "$lock_file" 2>/dev/null || true)"
218
+ if [[ "$lock_mtime" =~ ^[0-9]+$ ]]; then
219
+ if [[ -z "$activity_epoch" || "$lock_mtime" -gt "$activity_epoch" ]]; then
220
+ activity_epoch="$lock_mtime"
221
+ fi
222
+ fi
223
+ fi
224
+ fi
225
+
226
+ printf '%s' "$activity_epoch"
227
+ }
228
+
386
229
  skipped_recent=0
387
230
 
388
231
  branch_idle_gate() {
389
232
  local branch="$1"
390
233
  local wt="$2"
391
234
  local reason="$3"
392
- local subject=""
393
- local commit_epoch=""
394
- local age=0
395
- local wait_remaining=0
396
-
397
235
  if [[ "$IDLE_SECONDS" -le 0 ]]; then
398
236
  return 0
399
237
  fi
400
-
401
- if [[ -n "$branch" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"; then
402
- commit_epoch="$(git -C "$repo_root" log -1 --format=%ct "$branch" 2>/dev/null || true)"
403
- subject="$branch"
404
- elif [[ -n "$wt" ]]; then
405
- commit_epoch="$(git -C "$wt" log -1 --format=%ct 2>/dev/null || true)"
406
- subject="$wt"
238
+ if [[ -z "$branch" ]]; then
239
+ return 0
407
240
  fi
408
241
 
409
- if [[ -z "$commit_epoch" || ! "$commit_epoch" =~ ^[0-9]+$ ]]; then
242
+ local last_activity_epoch=""
243
+ last_activity_epoch="$(read_branch_activity_epoch "$branch" "$wt")"
244
+ if [[ ! "$last_activity_epoch" =~ ^[0-9]+$ ]]; then
410
245
  return 0
411
246
  fi
412
247
 
413
- age=$((NOW_EPOCH - commit_epoch))
414
- if (( age < 0 )); then
415
- age=0
248
+ local idle_age=$((NOW_EPOCH - last_activity_epoch))
249
+ if [[ "$idle_age" -lt 0 ]]; then
250
+ idle_age=0
416
251
  fi
417
-
418
- if (( age < IDLE_SECONDS )); then
419
- wait_remaining=$((IDLE_SECONDS - age))
252
+ if [[ "$idle_age" -lt "$IDLE_SECONDS" ]]; then
420
253
  skipped_recent=$((skipped_recent + 1))
421
- echo "[agent-worktree-prune] Skipping recent ${reason}: ${subject} (age=${age}s, threshold=${IDLE_SECONDS}s, wait~${wait_remaining}s)"
254
+ echo "[agent-worktree-prune] Skipping recent branch (${reason}): ${branch} (idle=${idle_age}s < ${IDLE_SECONDS}s)"
422
255
  return 1
423
256
  fi
424
-
425
257
  return 0
426
258
  }
427
259
 
@@ -484,10 +316,6 @@ removed_worktrees=0
484
316
  removed_branches=0
485
317
  skipped_active=0
486
318
  skipped_dirty=0
487
- repaired_detached_conflicts=0
488
- failed_ops=0
489
-
490
- relocate_foreign_worktree_entries
491
319
 
492
320
  relocate_foreign_worktree_entries
493
321
 
@@ -514,26 +342,20 @@ process_entry() {
514
342
  fi
515
343
 
516
344
  local remove_reason=""
517
- local wt_name
518
- wt_name="$(basename "$wt")"
519
345
 
520
- if [[ "$wt_name" == __integrate-* || "$wt_name" == __source-probe-* ]]; then
521
- remove_reason="temporary-worktree"
522
- elif [[ -z "$branch_ref" ]]; then
346
+ if [[ -z "$branch_ref" ]]; then
523
347
  remove_reason="detached-worktree"
524
348
  elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"; then
525
349
  remove_reason="missing-branch"
526
- elif is_agent_branch "$branch"; then
350
+ elif [[ "$branch" == agent/* ]]; then
527
351
  if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
528
352
  if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
529
353
  remove_reason="merged-agent-branch"
530
- else
531
- remove_reason="merged-agent-worktree"
532
354
  fi
533
355
  elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
534
356
  remove_reason="clean-agent-worktree"
535
357
  fi
536
- elif is_temporary_branch "$branch"; then
358
+ elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
537
359
  remove_reason="temporary-worktree"
538
360
  fi
539
361
 
@@ -545,75 +367,22 @@ process_entry() {
545
367
  return
546
368
  fi
547
369
 
548
- if [[ "$FORCE_DIRTY" -ne 1 ]] \
549
- && [[ "$remove_reason" == "detached-worktree" ]] \
550
- && has_unmerged_conflicts "$wt"; then
551
- local wt_component
552
- local base_component
553
- local recovery_seed
554
- local recovery_branch
555
-
556
- wt_component="$(sanitize_branch_component "$wt_name")"
557
- base_component="$(sanitize_branch_component "$BASE_BRANCH")"
558
- recovery_seed="agent/recover/${base_component}-${wt_component}-$(date +%Y%m%d-%H%M%S)"
559
- recovery_branch="$(resolve_unique_recovery_branch_name "$recovery_seed")"
560
-
561
- if [[ "$DRY_RUN" -eq 1 ]]; then
562
- echo "[agent-worktree-prune] [dry-run] Would recover detached conflicted worktree: ${wt} -> ${recovery_branch}"
563
- repaired_detached_conflicts=$((repaired_detached_conflicts + 1))
564
- return
565
- fi
566
-
567
- if git -C "$wt" checkout -b "$recovery_branch" >/dev/null 2>&1; then
568
- repaired_detached_conflicts=$((repaired_detached_conflicts + 1))
569
- echo "[agent-worktree-prune] Recovered detached conflicted worktree: ${wt} -> ${recovery_branch}"
570
- return
571
- fi
572
-
573
- failed_ops=$((failed_ops + 1))
574
- echo "[agent-worktree-prune] Failed to recover detached conflicted worktree: ${wt}" >&2
575
- return
576
- fi
577
-
578
370
  if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
579
- if [[ "$remove_reason" == "temporary-worktree" ]]; then
580
- # __integrate-* and __source-probe-* are disposable scaffolding owned by
581
- # agent-branch-finish. When they're dirty it's because a rebase/merge
582
- # aborted mid-flight; the underlying branch ref is untouched. Always
583
- # force-remove so stranded probes never accumulate in VS Code SCM.
584
- echo "[agent-worktree-prune] Force-removing dirty temporary worktree (${remove_reason}): ${wt}"
585
- git -C "$wt" rebase --abort >/dev/null 2>&1 || true
586
- git -C "$wt" merge --abort >/dev/null 2>&1 || true
587
- elif [[ "$remove_reason" == "merged-agent-branch" || "$remove_reason" == "merged-agent-worktree" ]] \
588
- && worktree_matches_bootstrap_manifest "$wt"; then
589
- # Bootstrap-manifest match means every dirty path is a scaffold file
590
- # unchanged since branch-start — safe to remove even without --branch.
591
- echo "[agent-worktree-prune] Treating bootstrap-only sandbox as safe to remove (${remove_reason}): ${wt}"
592
- elif [[ "$FORCE_MERGED" -eq 1 ]] \
593
- && [[ "$remove_reason" == "merged-agent-branch" || "$remove_reason" == "merged-agent-worktree" ]]; then
594
- echo "[agent-worktree-prune] Force-removing dirty merged worktree (${remove_reason}): ${wt}"
595
- else
596
- skipped_dirty=$((skipped_dirty + 1))
597
- echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
598
- return
599
- fi
371
+ skipped_dirty=$((skipped_dirty + 1))
372
+ echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
373
+ return
600
374
  fi
601
375
 
602
376
  echo "[agent-worktree-prune] Removing worktree (${remove_reason}): ${wt}"
603
- if run_cmd git -C "$repo_root" worktree remove "$wt" --force; then
604
- removed_worktrees=$((removed_worktrees + 1))
605
- else
606
- failed_ops=$((failed_ops + 1))
607
- echo "[agent-worktree-prune] Failed to remove worktree (${remove_reason}): ${wt}" >&2
608
- return
609
- fi
377
+ run_cmd git -C "$repo_root" worktree remove "$wt" --force
378
+ removed_worktrees=$((removed_worktrees + 1))
610
379
 
611
380
  if [[ -z "$branch" ]]; then
612
381
  return
613
382
  fi
614
383
 
615
384
  if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
616
- if is_agent_branch "$branch" && [[ "$DELETE_BRANCHES" -eq 1 ]]; then
385
+ if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
617
386
  if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
618
387
  removed_branches=$((removed_branches + 1))
619
388
  echo "[agent-worktree-prune] Deleted merged branch: ${branch}"
@@ -624,7 +393,7 @@ process_entry() {
624
393
  fi
625
394
  fi
626
395
  fi
627
- elif is_temporary_branch "$branch"; then
396
+ elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
628
397
  run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1 || true
629
398
  removed_branches=$((removed_branches + 1))
630
399
  echo "[agent-worktree-prune] Deleted temporary branch: ${branch}"
@@ -664,81 +433,27 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
664
433
  if branch_has_worktree "$branch"; then
665
434
  continue
666
435
  fi
667
- if is_temporary_branch "$branch"; then
668
- if ! branch_idle_gate "$branch" "" "stale-temporary-branch"; then
669
- continue
670
- fi
671
- if run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1; then
672
- removed_branches=$((removed_branches + 1))
673
- if [[ "$DRY_RUN" -eq 1 ]]; then
674
- echo "[agent-worktree-prune] Would delete stale temporary branch: ${branch}"
675
- else
676
- echo "[agent-worktree-prune] Deleted stale temporary branch: ${branch}"
677
- fi
678
- fi
679
- continue
680
- fi
681
- if ! is_agent_branch "$branch"; then
682
- continue
683
- fi
684
436
  if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
685
437
  continue
686
438
  fi
687
439
  if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
688
440
  if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
689
441
  removed_branches=$((removed_branches + 1))
690
- if [[ "$DRY_RUN" -eq 1 ]]; then
691
- echo "[agent-worktree-prune] Would delete stale merged branch: ${branch}"
692
- else
693
- echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
694
- fi
442
+ echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
695
443
  if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
696
444
  if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
697
445
  run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
698
- if [[ "$DRY_RUN" -eq 1 ]]; then
699
- echo "[agent-worktree-prune] Would delete stale merged remote branch: ${branch}"
700
- else
701
- echo "[agent-worktree-prune] Deleted stale merged remote branch: ${branch}"
702
- fi
446
+ echo "[agent-worktree-prune] Deleted stale merged remote branch: ${branch}"
703
447
  fi
704
448
  fi
705
449
  fi
706
450
  fi
707
- done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads | awk '/^agent\// || /^__agent_integrate_/ || /^__source-probe-/')
451
+ done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/agent)
708
452
  fi
709
453
 
710
- if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
711
- while IFS= read -r remote_ref; do
712
- [[ -z "$remote_ref" ]] && continue
713
- local_branch="${remote_ref#origin/}"
714
- if [[ -n "$TARGET_BRANCH" && "$local_branch" != "$TARGET_BRANCH" ]]; then
715
- continue
716
- fi
717
- if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${local_branch}"; then
718
- continue
719
- fi
720
- if ! is_agent_branch "$local_branch"; then
721
- continue
722
- fi
723
- if git -C "$repo_root" merge-base --is-ancestor "$remote_ref" "$BASE_BRANCH"; then
724
- if run_cmd git -C "$repo_root" push origin --delete "$local_branch" >/dev/null 2>&1; then
725
- removed_branches=$((removed_branches + 1))
726
- if [[ "$DRY_RUN" -eq 1 ]]; then
727
- echo "[agent-worktree-prune] Would delete stale merged remote-only branch: ${local_branch}"
728
- else
729
- echo "[agent-worktree-prune] Deleted stale merged remote-only branch: ${local_branch}"
730
- fi
731
- fi
732
- fi
733
- done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/remotes/origin/agent)
734
- fi
454
+ run_cmd git -C "$repo_root" worktree prune
735
455
 
736
- if ! run_cmd git -C "$repo_root" worktree prune; then
737
- failed_ops=$((failed_ops + 1))
738
- echo "[agent-worktree-prune] Warning: git worktree prune failed." >&2
739
- fi
740
-
741
- echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}, repaired_detached_conflicts=${repaired_detached_conflicts}"
456
+ echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, idle_minutes=${IDLE_MINUTES}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}, skipped_recent=${skipped_recent}"
742
457
  if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
743
458
  echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
744
459
  fi
@@ -746,11 +461,8 @@ if [[ "$skipped_active" -gt 0 ]]; then
746
461
  echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
747
462
  fi
748
463
  if [[ "$skipped_dirty" -gt 0 ]]; then
749
- echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, pass --force-merged to remove dirty worktrees whose branch is already merged into ${BASE_BRANCH}, or pass --force-dirty to remove any dirty worktree." >&2
464
+ echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
750
465
  fi
751
466
  if [[ "$IDLE_SECONDS" -gt 0 && "$skipped_recent" -gt 0 ]]; then
752
467
  echo "[agent-worktree-prune] Tip: recent branches were preserved by --idle-minutes=${IDLE_MINUTES}. Re-run later or lower the threshold." >&2
753
468
  fi
754
- if [[ "$failed_ops" -gt 0 ]]; then
755
- echo "[agent-worktree-prune] Tip: some cleanup operations failed and were skipped. Re-run after fixing file-system or permission blockers." >&2
756
- fi