@imdeadpool/guardex 6.0.0 → 6.1.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.
- package/bin/multiagent-safety.js +21 -5
- package/package.json +3 -1
- package/templates/AGENTS.multiagent-safety.md +1 -0
- package/templates/githooks/post-checkout +68 -0
- package/templates/githooks/post-merge +3 -39
- package/templates/githooks/pre-commit +193 -27
- package/templates/githooks/pre-push +0 -0
- package/templates/scripts/agent-branch-finish.sh +702 -70
- package/templates/scripts/agent-branch-start.sh +877 -76
- package/templates/scripts/agent-worktree-prune.sh +353 -65
- package/templates/scripts/codex-agent.sh +238 -626
- package/templates/scripts/install-agent-git-hooks.sh +27 -4
- package/templates/scripts/openspec/init-change-workspace.sh +50 -4
- package/templates/scripts/openspec/init-plan-workspace.sh +495 -48
- package/templates/scripts/review-bot-watch.sh +11 -11
|
@@ -5,6 +5,7 @@ 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
|
|
8
9
|
DELETE_BRANCHES=0
|
|
9
10
|
DELETE_REMOTE_BRANCHES=0
|
|
10
11
|
ONLY_DIRTY_WORKTREES=0
|
|
@@ -33,6 +34,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
33
34
|
FORCE_DIRTY=1
|
|
34
35
|
shift
|
|
35
36
|
;;
|
|
37
|
+
--force-merged)
|
|
38
|
+
FORCE_MERGED=1
|
|
39
|
+
shift
|
|
40
|
+
;;
|
|
36
41
|
--delete-branches)
|
|
37
42
|
DELETE_BRANCHES=1
|
|
38
43
|
shift
|
|
@@ -55,7 +60,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
55
60
|
;;
|
|
56
61
|
*)
|
|
57
62
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
58
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent
|
|
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
|
|
59
64
|
exit 1
|
|
60
65
|
;;
|
|
61
66
|
esac
|
|
@@ -66,7 +71,15 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
66
71
|
exit 1
|
|
67
72
|
fi
|
|
68
73
|
|
|
69
|
-
|
|
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)"
|
|
70
83
|
current_pwd="$(pwd -P)"
|
|
71
84
|
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
72
85
|
repo_common_dir="$(
|
|
@@ -101,13 +114,28 @@ resolve_base_branch() {
|
|
|
101
114
|
printf '%s' ""
|
|
102
115
|
}
|
|
103
116
|
|
|
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
|
+
|
|
104
132
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
105
133
|
echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
|
|
106
134
|
exit 1
|
|
107
135
|
fi
|
|
108
136
|
|
|
109
|
-
if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH"
|
|
110
|
-
echo "[agent-worktree-prune] --branch must reference
|
|
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
|
|
111
139
|
exit 1
|
|
112
140
|
fi
|
|
113
141
|
|
|
@@ -152,7 +180,10 @@ run_cmd() {
|
|
|
152
180
|
|
|
153
181
|
branch_has_worktree() {
|
|
154
182
|
local branch="$1"
|
|
155
|
-
git -C "$repo_root" worktree list --porcelain |
|
|
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
|
+
'
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
is_clean_worktree() {
|
|
@@ -162,6 +193,166 @@ is_clean_worktree() {
|
|
|
162
193
|
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
|
|
163
194
|
}
|
|
164
195
|
|
|
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
|
+
|
|
165
356
|
resolve_worktree_common_dir() {
|
|
166
357
|
local wt="$1"
|
|
167
358
|
local common_dir=""
|
|
@@ -192,68 +383,45 @@ select_unique_worktree_path() {
|
|
|
192
383
|
printf '%s' "$candidate"
|
|
193
384
|
}
|
|
194
385
|
|
|
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
|
-
|
|
229
386
|
skipped_recent=0
|
|
230
387
|
|
|
231
388
|
branch_idle_gate() {
|
|
232
389
|
local branch="$1"
|
|
233
390
|
local wt="$2"
|
|
234
391
|
local reason="$3"
|
|
392
|
+
local subject=""
|
|
393
|
+
local commit_epoch=""
|
|
394
|
+
local age=0
|
|
395
|
+
local wait_remaining=0
|
|
396
|
+
|
|
235
397
|
if [[ "$IDLE_SECONDS" -le 0 ]]; then
|
|
236
398
|
return 0
|
|
237
399
|
fi
|
|
238
|
-
|
|
239
|
-
|
|
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"
|
|
240
407
|
fi
|
|
241
408
|
|
|
242
|
-
|
|
243
|
-
last_activity_epoch="$(read_branch_activity_epoch "$branch" "$wt")"
|
|
244
|
-
if [[ ! "$last_activity_epoch" =~ ^[0-9]+$ ]]; then
|
|
409
|
+
if [[ -z "$commit_epoch" || ! "$commit_epoch" =~ ^[0-9]+$ ]]; then
|
|
245
410
|
return 0
|
|
246
411
|
fi
|
|
247
412
|
|
|
248
|
-
|
|
249
|
-
if
|
|
250
|
-
|
|
413
|
+
age=$((NOW_EPOCH - commit_epoch))
|
|
414
|
+
if (( age < 0 )); then
|
|
415
|
+
age=0
|
|
251
416
|
fi
|
|
252
|
-
|
|
417
|
+
|
|
418
|
+
if (( age < IDLE_SECONDS )); then
|
|
419
|
+
wait_remaining=$((IDLE_SECONDS - age))
|
|
253
420
|
skipped_recent=$((skipped_recent + 1))
|
|
254
|
-
echo "[agent-worktree-prune] Skipping recent
|
|
421
|
+
echo "[agent-worktree-prune] Skipping recent ${reason}: ${subject} (age=${age}s, threshold=${IDLE_SECONDS}s, wait~${wait_remaining}s)"
|
|
255
422
|
return 1
|
|
256
423
|
fi
|
|
424
|
+
|
|
257
425
|
return 0
|
|
258
426
|
}
|
|
259
427
|
|
|
@@ -316,6 +484,10 @@ removed_worktrees=0
|
|
|
316
484
|
removed_branches=0
|
|
317
485
|
skipped_active=0
|
|
318
486
|
skipped_dirty=0
|
|
487
|
+
repaired_detached_conflicts=0
|
|
488
|
+
failed_ops=0
|
|
489
|
+
|
|
490
|
+
relocate_foreign_worktree_entries
|
|
319
491
|
|
|
320
492
|
relocate_foreign_worktree_entries
|
|
321
493
|
|
|
@@ -342,20 +514,26 @@ process_entry() {
|
|
|
342
514
|
fi
|
|
343
515
|
|
|
344
516
|
local remove_reason=""
|
|
517
|
+
local wt_name
|
|
518
|
+
wt_name="$(basename "$wt")"
|
|
345
519
|
|
|
346
|
-
if [[
|
|
520
|
+
if [[ "$wt_name" == __integrate-* || "$wt_name" == __source-probe-* ]]; then
|
|
521
|
+
remove_reason="temporary-worktree"
|
|
522
|
+
elif [[ -z "$branch_ref" ]]; then
|
|
347
523
|
remove_reason="detached-worktree"
|
|
348
524
|
elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"; then
|
|
349
525
|
remove_reason="missing-branch"
|
|
350
|
-
elif
|
|
526
|
+
elif is_agent_branch "$branch"; then
|
|
351
527
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
352
528
|
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
353
529
|
remove_reason="merged-agent-branch"
|
|
530
|
+
else
|
|
531
|
+
remove_reason="merged-agent-worktree"
|
|
354
532
|
fi
|
|
355
533
|
elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
|
|
356
534
|
remove_reason="clean-agent-worktree"
|
|
357
535
|
fi
|
|
358
|
-
elif
|
|
536
|
+
elif is_temporary_branch "$branch"; then
|
|
359
537
|
remove_reason="temporary-worktree"
|
|
360
538
|
fi
|
|
361
539
|
|
|
@@ -367,22 +545,75 @@ process_entry() {
|
|
|
367
545
|
return
|
|
368
546
|
fi
|
|
369
547
|
|
|
370
|
-
if [[ "$FORCE_DIRTY" -ne 1 ]]
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
373
575
|
return
|
|
374
576
|
fi
|
|
375
577
|
|
|
578
|
+
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
|
|
600
|
+
fi
|
|
601
|
+
|
|
376
602
|
echo "[agent-worktree-prune] Removing worktree (${remove_reason}): ${wt}"
|
|
377
|
-
run_cmd git -C "$repo_root" worktree remove "$wt" --force
|
|
378
|
-
|
|
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
|
|
379
610
|
|
|
380
611
|
if [[ -z "$branch" ]]; then
|
|
381
612
|
return
|
|
382
613
|
fi
|
|
383
614
|
|
|
384
615
|
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
|
|
385
|
-
if
|
|
616
|
+
if is_agent_branch "$branch" && [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
386
617
|
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
387
618
|
removed_branches=$((removed_branches + 1))
|
|
388
619
|
echo "[agent-worktree-prune] Deleted merged branch: ${branch}"
|
|
@@ -393,7 +624,7 @@ process_entry() {
|
|
|
393
624
|
fi
|
|
394
625
|
fi
|
|
395
626
|
fi
|
|
396
|
-
elif
|
|
627
|
+
elif is_temporary_branch "$branch"; then
|
|
397
628
|
run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1 || true
|
|
398
629
|
removed_branches=$((removed_branches + 1))
|
|
399
630
|
echo "[agent-worktree-prune] Deleted temporary branch: ${branch}"
|
|
@@ -433,27 +664,81 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
|
433
664
|
if branch_has_worktree "$branch"; then
|
|
434
665
|
continue
|
|
435
666
|
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
|
|
436
684
|
if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
|
|
437
685
|
continue
|
|
438
686
|
fi
|
|
439
687
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
440
688
|
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
441
689
|
removed_branches=$((removed_branches + 1))
|
|
442
|
-
|
|
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
|
|
443
695
|
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
444
696
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
445
697
|
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
446
|
-
|
|
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
|
|
447
703
|
fi
|
|
448
704
|
fi
|
|
449
705
|
fi
|
|
450
706
|
fi
|
|
451
|
-
done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/
|
|
707
|
+
done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads | awk '/^agent\// || /^__agent_integrate_/ || /^__source-probe-/')
|
|
452
708
|
fi
|
|
453
709
|
|
|
454
|
-
|
|
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
|
|
455
735
|
|
|
456
|
-
|
|
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}"
|
|
457
742
|
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
|
|
458
743
|
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
|
|
459
744
|
fi
|
|
@@ -461,8 +746,11 @@ if [[ "$skipped_active" -gt 0 ]]; then
|
|
|
461
746
|
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
|
|
462
747
|
fi
|
|
463
748
|
if [[ "$skipped_dirty" -gt 0 ]]; then
|
|
464
|
-
echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove
|
|
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
|
|
465
750
|
fi
|
|
466
751
|
if [[ "$IDLE_SECONDS" -gt 0 && "$skipped_recent" -gt 0 ]]; then
|
|
467
752
|
echo "[agent-worktree-prune] Tip: recent branches were preserved by --idle-minutes=${IDLE_MINUTES}. Re-run later or lower the threshold." >&2
|
|
468
753
|
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
|