@imdeadpool/guardex 6.1.0 → 7.0.1
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/README.md +63 -39
- package/bin/multiagent-safety.js +372 -296
- package/package.json +3 -5
- package/templates/AGENTS.multiagent-safety.md +9 -82
- package/templates/claude/commands/guardex.md +6 -12
- package/templates/codex/skills/guardex/SKILL.md +18 -64
- package/templates/githooks/post-merge +39 -3
- package/templates/githooks/pre-commit +27 -193
- package/templates/githooks/pre-push +0 -0
- package/templates/scripts/agent-branch-finish.sh +70 -702
- package/templates/scripts/agent-branch-start.sh +76 -877
- package/templates/scripts/agent-worktree-prune.sh +65 -353
- package/templates/scripts/codex-agent.sh +626 -238
- package/templates/scripts/install-agent-git-hooks.sh +4 -27
- package/templates/scripts/openspec/init-change-workspace.sh +4 -50
- package/templates/scripts/openspec/init-plan-workspace.sh +48 -495
- package/templates/scripts/review-bot-watch.sh +11 -11
|
@@ -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] [--
|
|
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
|
-
|
|
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"
|
|
138
|
-
echo "[agent-worktree-prune] --branch must reference agent
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
if
|
|
415
|
-
|
|
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}: ${
|
|
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 [[
|
|
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
|
|
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
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
451
|
+
done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/agent)
|
|
708
452
|
fi
|
|
709
453
|
|
|
710
|
-
|
|
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
|
-
|
|
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,
|
|
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
|