@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.
@@ -4,7 +4,6 @@ set -euo pipefail
4
4
  BASE_BRANCH=""
5
5
  BASE_BRANCH_EXPLICIT=0
6
6
  SOURCE_BRANCH=""
7
- SOURCE_BRANCH_EXPLICIT=0
8
7
  PUSH_ENABLED=1
9
8
  DELETE_REMOTE_BRANCH=0
10
9
  DELETE_REMOTE_BRANCH_EXPLICIT=0
@@ -14,14 +13,6 @@ CLEANUP_AFTER_MERGE_RAW="${GUARDEX_FINISH_CLEANUP:-false}"
14
13
  WAIT_FOR_MERGE_RAW="${GUARDEX_FINISH_WAIT_FOR_MERGE:-false}"
15
14
  WAIT_TIMEOUT_SECONDS_RAW="${GUARDEX_FINISH_WAIT_TIMEOUT_SECONDS:-1800}"
16
15
  WAIT_POLL_SECONDS_RAW="${GUARDEX_FINISH_WAIT_POLL_SECONDS:-10}"
17
- REQUIRE_REMOTE_GATES_RAW="${GUARDEX_REQUIRE_REMOTE_GATES:-false}"
18
- ENFORCE_AGENT_CLEANUP_RAW="${GUARDEX_ENFORCE_AGENT_CLEANUP:-true}"
19
- SKIP_TASKS_GATE_RAW="${GUARDEX_SKIP_TASKS_GATE:-false}"
20
- PR_REF="${GUARDEX_GH_PR_REF:-}"
21
- GH_REPO_REF="${GUARDEX_GH_REPO:-}"
22
- NO_CLEANUP_REQUESTED=0
23
- TIER_LEVEL_RAW="${GUARDEX_TIER:-}"
24
- TIER_LEVEL_EXPLICIT=0
25
16
 
26
17
  normalize_bool() {
27
18
  local raw="${1:-}"
@@ -53,62 +44,10 @@ normalize_int() {
53
44
  printf '%s' "$value"
54
45
  }
55
46
 
56
- normalize_tier() {
57
- local raw="${1:-}"
58
- local upper
59
- upper="$(printf '%s' "$raw" | tr '[:lower:]' '[:upper:]')"
60
- case "$upper" in
61
- T0|T1|T2|T3) printf '%s' "$upper" ;;
62
- 0) printf 'T0' ;;
63
- 1) printf 'T1' ;;
64
- 2) printf 'T2' ;;
65
- 3) printf 'T3' ;;
66
- '') printf '' ;;
67
- *)
68
- echo "[agent-branch-finish] Unknown tier: ${raw} (expected T0|T1|T2|T3)" >&2
69
- return 1
70
- ;;
71
- esac
72
- }
73
-
74
- resolve_tier_from_manifest() {
75
- local worktree="$1"
76
- local git_dir manifest_path
77
- git_dir="$(git -C "$worktree" rev-parse --git-dir 2>/dev/null || true)"
78
- if [[ -z "$git_dir" ]]; then
79
- return 1
80
- fi
81
- if [[ "$git_dir" != /* ]]; then
82
- git_dir="$(cd "$worktree/$git_dir" 2>/dev/null && pwd -P || true)"
83
- fi
84
- manifest_path="${git_dir}/guardex-bootstrap-manifest.json"
85
- if [[ ! -f "$manifest_path" ]]; then
86
- return 1
87
- fi
88
- python3 - "$manifest_path" <<'PY' 2>/dev/null || return 1
89
- import json
90
- import sys
91
-
92
- try:
93
- data = json.loads(open(sys.argv[1]).read())
94
- except Exception:
95
- sys.exit(1)
96
- tier = data.get("tier")
97
- if isinstance(tier, str) and tier:
98
- print(tier)
99
- else:
100
- sys.exit(1)
101
- PY
102
- }
103
-
104
47
  CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
105
48
  WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "0")"
106
49
  WAIT_TIMEOUT_SECONDS="$(normalize_int "$WAIT_TIMEOUT_SECONDS_RAW" "1800" "30")"
107
50
  WAIT_POLL_SECONDS="$(normalize_int "$WAIT_POLL_SECONDS_RAW" "10" "0")"
108
- REQUIRE_REMOTE_GATES="$(normalize_bool "$REQUIRE_REMOTE_GATES_RAW" "0")"
109
- ENFORCE_AGENT_CLEANUP="$(normalize_bool "$ENFORCE_AGENT_CLEANUP_RAW" "1")"
110
- SKIP_TASKS_GATE="$(normalize_bool "$SKIP_TASKS_GATE_RAW" "0")"
111
- TIER_LEVEL=""
112
51
 
113
52
  while [[ $# -gt 0 ]]; do
114
53
  case "$1" in
@@ -119,7 +58,6 @@ while [[ $# -gt 0 ]]; do
119
58
  ;;
120
59
  --branch)
121
60
  SOURCE_BRANCH="${2:-}"
122
- SOURCE_BRANCH_EXPLICIT=1
123
61
  shift 2
124
62
  ;;
125
63
  --no-push)
@@ -142,7 +80,6 @@ while [[ $# -gt 0 ]]; do
142
80
  ;;
143
81
  --no-cleanup)
144
82
  CLEANUP_AFTER_MERGE=0
145
- NO_CLEANUP_REQUESTED=1
146
83
  shift
147
84
  ;;
148
85
  --wait-for-merge)
@@ -165,22 +102,6 @@ while [[ $# -gt 0 ]]; do
165
102
  MERGE_MODE="${2:-auto}"
166
103
  shift 2
167
104
  ;;
168
- --pr)
169
- PR_REF="${2:-}"
170
- shift 2
171
- ;;
172
- --repo)
173
- GH_REPO_REF="${2:-}"
174
- shift 2
175
- ;;
176
- --require-remote-gates)
177
- REQUIRE_REMOTE_GATES=1
178
- shift
179
- ;;
180
- --no-require-remote-gates)
181
- REQUIRE_REMOTE_GATES=0
182
- shift
183
- ;;
184
105
  --via-pr)
185
106
  MERGE_MODE="pr"
186
107
  shift
@@ -189,23 +110,18 @@ while [[ $# -gt 0 ]]; do
189
110
  MERGE_MODE="direct"
190
111
  shift
191
112
  ;;
192
- --skip-tasks-gate)
193
- SKIP_TASKS_GATE=1
194
- shift
195
- ;;
196
- --tier)
197
- TIER_LEVEL_RAW="${2:-}"
198
- TIER_LEVEL_EXPLICIT=1
199
- shift 2
200
- ;;
201
113
  *)
202
114
  echo "[agent-branch-finish] Unknown argument: $1" >&2
203
- echo "Usage: $0 [--base <branch>] [--branch <branch>] [--tier T0|T1|T2|T3] [--no-push] [--cleanup|--no-cleanup] [--wait-for-merge|--no-wait-for-merge] [--wait-timeout-seconds <n>] [--wait-poll-seconds <n>] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only] [--pr <ref>] [--repo <owner/name>] [--require-remote-gates|--no-require-remote-gates]" >&2
115
+ echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--cleanup|--no-cleanup] [--wait-for-merge|--no-wait-for-merge] [--wait-timeout-seconds <n>] [--wait-poll-seconds <n>] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
204
116
  exit 1
205
117
  ;;
206
118
  esac
207
119
  done
208
120
 
121
+ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
122
+ DELETE_REMOTE_BRANCH=1
123
+ fi
124
+
209
125
  case "$MERGE_MODE" in
210
126
  auto|direct|pr) ;;
211
127
  *)
@@ -230,65 +146,10 @@ fi
230
146
  repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
231
147
  agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
232
148
 
233
- infer_agent_branch_from_worktree_path() {
234
- local wt_path="$1"
235
- local wt_name=""
236
- local suffix=""
237
- local candidate=""
238
-
239
- if [[ "$wt_path" != "${agent_worktree_root}"/* ]]; then
240
- return 1
241
- fi
242
-
243
- wt_name="$(basename "$wt_path")"
244
- if [[ "$wt_name" != agent__* ]]; then
245
- return 1
246
- fi
247
-
248
- suffix="${wt_name#agent__}"
249
- candidate="agent/${suffix//__//}"
250
- if [[ ! "$candidate" =~ ^agent/[A-Za-z0-9._/-]+$ ]]; then
251
- return 1
252
- fi
253
- printf '%s' "$candidate"
254
- }
255
-
256
149
  if [[ -z "$SOURCE_BRANCH" ]]; then
257
150
  SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
258
151
  fi
259
152
 
260
- if [[ "$SOURCE_BRANCH_EXPLICIT" -eq 0 && "$SOURCE_BRANCH" == "HEAD" ]]; then
261
- detached_hint_branch=""
262
- detached_recover_cmd=""
263
- detached_recover_branch=""
264
- detached_conflicts="$(git -C "$current_worktree" diff --name-only --diff-filter=U 2>/dev/null || true)"
265
-
266
- detached_hint_branch="$(infer_agent_branch_from_worktree_path "$current_worktree" || true)"
267
- if [[ -n "$detached_hint_branch" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${detached_hint_branch}"; then
268
- detached_recover_cmd="git -C \"$current_worktree\" checkout \"$detached_hint_branch\""
269
- elif [[ -n "$detached_hint_branch" ]]; then
270
- detached_recover_cmd="git -C \"$current_worktree\" checkout -b \"$detached_hint_branch\""
271
- else
272
- detached_recover_branch="agent/recover/detached-$(date +%Y%m%d-%H%M%S)"
273
- detached_recover_cmd="git -C \"$current_worktree\" checkout -b \"$detached_recover_branch\""
274
- fi
275
-
276
- echo "[agent-branch-finish] Current worktree is in detached HEAD; finish requires a branch context." >&2
277
- if [[ -n "$detached_conflicts" ]]; then
278
- echo "[agent-branch-finish] Unmerged files detected in this detached worktree:" >&2
279
- while IFS= read -r file; do
280
- [[ -n "$file" ]] && echo " - ${file}" >&2
281
- done <<< "$detached_conflicts"
282
- fi
283
- echo "[agent-branch-finish] Recover branch context with: ${detached_recover_cmd}" >&2
284
- if [[ -n "$detached_hint_branch" ]]; then
285
- echo "[agent-branch-finish] Then resolve/commit and rerun finish with: bash scripts/agent-branch-finish.sh --branch \"${detached_hint_branch}\" --base dev --via-pr --wait-for-merge --cleanup" >&2
286
- else
287
- echo "[agent-branch-finish] Then resolve/commit and rerun finish with --branch <your-recovered-agent-branch>." >&2
288
- fi
289
- exit 1
290
- fi
291
-
292
153
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
293
154
  echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
294
155
  exit 1
@@ -301,28 +162,6 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
301
162
  fi
302
163
  fi
303
164
 
304
- if [[ -z "$BASE_BRANCH" ]]; then
305
- branch_stored_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexBase" || true)"
306
- if [[ -n "$branch_stored_base" ]]; then
307
- BASE_BRANCH="$branch_stored_base"
308
- fi
309
- fi
310
-
311
- if [[ -z "$BASE_BRANCH" ]]; then
312
- source_upstream="$(git -C "$repo_root" for-each-ref --count=1 --format='%(upstream:short)' "refs/heads/${SOURCE_BRANCH}" || true)"
313
- source_upstream="${source_upstream:-}"
314
- if [[ "$source_upstream" == */* ]]; then
315
- BASE_BRANCH="${source_upstream#*/}"
316
- fi
317
- fi
318
-
319
- if [[ -z "$BASE_BRANCH" ]]; then
320
- current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
321
- if [[ -n "$current_branch" && "$current_branch" != "HEAD" && "$current_branch" != "$SOURCE_BRANCH" ]]; then
322
- BASE_BRANCH="$current_branch"
323
- fi
324
- fi
325
-
326
165
  if [[ -z "$BASE_BRANCH" ]]; then
327
166
  BASE_BRANCH="dev"
328
167
  fi
@@ -333,32 +172,6 @@ if [[ "$SOURCE_BRANCH" == "$BASE_BRANCH" ]]; then
333
172
  exit 1
334
173
  fi
335
174
 
336
- cleanup_mandatory=0
337
- if [[ "$ENFORCE_AGENT_CLEANUP" -eq 1 && "$PUSH_ENABLED" -eq 1 && "$SOURCE_BRANCH" =~ ^agent/ ]]; then
338
- cleanup_mandatory=1
339
- fi
340
-
341
- if [[ "$cleanup_mandatory" -eq 1 ]]; then
342
- if [[ "$CLEANUP_AFTER_MERGE" -ne 1 ]]; then
343
- if [[ "$NO_CLEANUP_REQUESTED" -eq 1 ]]; then
344
- echo "[agent-branch-finish] Ignoring --no-cleanup for '${SOURCE_BRANCH}': cleanup is mandatory for merged agent branches." >&2
345
- else
346
- echo "[agent-branch-finish] Enforcing mandatory cleanup for merged agent branch '${SOURCE_BRANCH}'." >&2
347
- fi
348
- CLEANUP_AFTER_MERGE=1
349
- fi
350
- if [[ "$DELETE_REMOTE_BRANCH" -ne 1 ]]; then
351
- if [[ "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 1 ]]; then
352
- echo "[agent-branch-finish] Ignoring --keep-remote-branch for '${SOURCE_BRANCH}': remote branch deletion is required by cleanup policy." >&2
353
- fi
354
- DELETE_REMOTE_BRANCH=1
355
- fi
356
- fi
357
-
358
- if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
359
- DELETE_REMOTE_BRANCH=1
360
- fi
361
-
362
175
  if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${SOURCE_BRANCH}"; then
363
176
  echo "[agent-branch-finish] Local source branch does not exist: ${SOURCE_BRANCH}" >&2
364
177
  exit 1
@@ -378,188 +191,9 @@ is_clean_worktree() {
378
191
  && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
379
192
  }
380
193
 
381
- validate_openspec_tasks_gate() {
382
- local branch="$1"
383
- local branch_root="$2"
384
- local helper_base=""
385
-
386
- if [[ ! "$branch" =~ ^agent/ ]]; then
387
- return 0
388
- fi
389
-
390
- helper_base="$(git -C "$repo_root" config --get "branch.${branch}.guardexBase" || true)"
391
- if [[ "$BASE_BRANCH" == agent/* ]] || [[ "$helper_base" == agent/* ]]; then
392
- if [[ -z "$helper_base" && "$BASE_BRANCH" == agent/* ]]; then
393
- helper_base="$BASE_BRANCH"
394
- fi
395
- echo "[agent-branch-finish] Skipping OpenSpec tasks gate for helper branch '${branch}' (base '${helper_base}')." >&2
396
- return 0
397
- fi
398
-
399
- local change_slug="${branch//\//-}"
400
- local tasks_file="${branch_root}/openspec/changes/${change_slug}/tasks.md"
401
- local use_collaboration_flow=0
402
- local cleanup_step="4"
403
- local required_section_labels=(
404
- "## 1. Specification"
405
- "## 2. Implementation"
406
- "## 3. Verification"
407
- )
408
- local required_section_patterns=(
409
- '^## 1\. Specification([[:space:]].*)?$'
410
- '^## 2\. Implementation([[:space:]].*)?$'
411
- '^## 3\. Verification([[:space:]].*)?$'
412
- )
413
-
414
- if [[ ! -f "$tasks_file" ]]; then
415
- echo "[agent-branch-finish] OpenSpec tasks gate failed for '${branch}'." >&2
416
- echo "[agent-branch-finish] Missing required file: openspec/changes/${change_slug}/tasks.md" >&2
417
- echo "[agent-branch-finish] Finish is blocked until the checklist file exists and is fully updated." >&2
418
- exit 1
419
- fi
420
-
421
- if grep -Eq '^## 4\. Collaboration([[:space:]].*)?$' "$tasks_file" && grep -Eq '^## 5\. Cleanup([[:space:]].*)?$' "$tasks_file"; then
422
- use_collaboration_flow=1
423
- cleanup_step="5"
424
- required_section_labels+=("## 4. Collaboration" "## 5. Cleanup")
425
- required_section_patterns+=('^## 4\. Collaboration([[:space:]].*)?$' '^## 5\. Cleanup([[:space:]].*)?$')
426
- else
427
- required_section_labels+=("## 4. Cleanup")
428
- required_section_patterns+=('^## 4\. Cleanup([[:space:]].*)?$')
429
- fi
430
-
431
- local missing_section=0
432
- local i
433
- for i in "${!required_section_labels[@]}"; do
434
- local section_label="${required_section_labels[$i]}"
435
- local section_pattern="${required_section_patterns[$i]}"
436
- if ! grep -Eq "$section_pattern" "$tasks_file"; then
437
- missing_section=1
438
- echo "[agent-branch-finish] OpenSpec tasks gate failed for '${branch}'." >&2
439
- echo "[agent-branch-finish] Missing required section in ${tasks_file}: ${section_label}" >&2
440
- fi
441
- done
442
- if [[ "$missing_section" -eq 1 ]]; then
443
- echo "[agent-branch-finish] Finish is blocked until all required checklist sections are present." >&2
444
- exit 1
445
- fi
446
-
447
- if ! grep -Eq "^[[:space:]]*-[[:space:]]*\\[[ xX]\\][[:space:]]*${cleanup_step}\\.1\\b" "$tasks_file"; then
448
- echo "[agent-branch-finish] OpenSpec tasks gate failed for '${branch}'." >&2
449
- echo "[agent-branch-finish] Missing required cleanup readiness item in ${tasks_file}: ${cleanup_step}.1" >&2
450
- echo "[agent-branch-finish] Finish is blocked until cleanup item ${cleanup_step}.1 is present." >&2
451
- exit 1
452
- fi
453
-
454
- local gate_unchecked
455
- gate_unchecked="$(awk -v collab_flow="$use_collaboration_flow" '
456
- BEGIN { scope = "" }
457
- /^## 1\. Specification([[:space:]].*)?$/ { scope = "spec"; next }
458
- /^## 2\. Implementation([[:space:]].*)?$/ { scope = "impl"; next }
459
- /^## 3\. Verification([[:space:]].*)?$/ { scope = "verify"; next }
460
- collab_flow == 1 && /^## 4\. Collaboration([[:space:]].*)?$/ { scope = "collaboration"; next }
461
- collab_flow == 1 && /^## 5\. Cleanup([[:space:]].*)?$/ { scope = "cleanup"; next }
462
- collab_flow != 1 && /^## 4\. Cleanup([[:space:]].*)?$/ { scope = "cleanup"; next }
463
- /^## / { scope = "" }
464
-
465
- scope ~ /^(spec|impl|verify)$/ && /^[[:space:]]*-[[:space:]]*\[ \]/ {
466
- print NR ":" $0
467
- next
468
- }
469
- ' "$tasks_file")"
470
-
471
- if [[ -n "$gate_unchecked" ]]; then
472
- echo "[agent-branch-finish] OpenSpec tasks gate failed for '${branch}'." >&2
473
- echo "[agent-branch-finish] Unchecked checklist items remain in ${tasks_file}:" >&2
474
- while IFS= read -r line; do
475
- [[ -n "$line" ]] && echo " - ${line}" >&2
476
- done <<< "$gate_unchecked"
477
- echo "[agent-branch-finish] Finish is blocked until all items in sections 1-3 are marked [x]." >&2
478
- exit 1
479
- fi
480
-
481
- if [[ "$use_collaboration_flow" -eq 1 ]]; then
482
- local collaboration_section=""
483
- collaboration_section="$(awk '
484
- BEGIN { in_collaboration = 0 }
485
- /^## 4\. Collaboration([[:space:]].*)?$/ { in_collaboration = 1; next }
486
- in_collaboration && /^## / { exit }
487
- in_collaboration { print }
488
- ' "$tasks_file")"
489
-
490
- local collaboration_ack_done=0
491
- local collaboration_na_done=0
492
- if grep -Eiq '^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*4\.3\b' <<<"$collaboration_section"; then
493
- collaboration_ack_done=1
494
- fi
495
- if grep -Eiq '^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*4\.4\b.*n[[:space:]]*/[[:space:]]*a' <<<"$collaboration_section"; then
496
- collaboration_na_done=1
497
- fi
498
-
499
- if [[ "$collaboration_ack_done" -ne 1 && "$collaboration_na_done" -ne 1 ]]; then
500
- echo "[agent-branch-finish] OpenSpec tasks gate failed for '${branch}'." >&2
501
- echo "[agent-branch-finish] Collaboration section in ${tasks_file} requires one completion path before cleanup:" >&2
502
- echo " - [x] 4.3 Owner Codex acknowledges joined outputs (accept/revise/reject), OR" >&2
503
- echo " - [x] 4.4 explicitly marked with N/A when no Codex joined." >&2
504
- echo "[agent-branch-finish] Finish is blocked until section 4 records one of these paths." >&2
505
- exit 1
506
- fi
507
- fi
508
- }
509
-
510
194
  source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
511
195
  created_source_probe=0
512
196
  source_probe_path=""
513
- integration_worktree=""
514
- integration_branch=""
515
- transient_worktrees_released=0
516
-
517
- release_transient_worktrees() {
518
- if [[ "$transient_worktrees_released" -eq 1 ]]; then
519
- return
520
- fi
521
- if [[ -n "$integration_worktree" && -d "$integration_worktree" ]]; then
522
- git -C "$repo_root" worktree remove "$integration_worktree" --force >/dev/null 2>&1 || true
523
- fi
524
- if [[ -n "$integration_branch" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; then
525
- local integration_branch_worktree=""
526
- integration_branch_worktree="$(get_worktree_for_branch "$integration_branch" || true)"
527
- if [[ -z "$integration_branch_worktree" ]]; then
528
- git -C "$repo_root" branch -D "$integration_branch" >/dev/null 2>&1 || true
529
- fi
530
- fi
531
- if [[ "$created_source_probe" -eq 1 && -n "$source_probe_path" && -d "$source_probe_path" ]]; then
532
- # Abort any in-progress rebase/merge so the branch ref is left at pre-op state
533
- # before removing the worktree. This prevents stranded mid-rebase sandboxes
534
- # when the auto-sync/preflight steps below exit on conflict.
535
- git -C "$source_probe_path" rebase --abort >/dev/null 2>&1 || true
536
- git -C "$source_probe_path" merge --abort >/dev/null 2>&1 || true
537
- git -C "$repo_root" worktree remove "$source_probe_path" --force >/dev/null 2>&1 || true
538
- fi
539
- if [[ "$created_source_probe" -eq 1 ]]; then
540
- source_worktree="$repo_root"
541
- created_source_probe=0
542
- source_probe_path=""
543
- fi
544
- transient_worktrees_released=1
545
- }
546
-
547
- cleanup() {
548
- release_transient_worktrees
549
- }
550
-
551
- handle_interrupt() {
552
- cleanup
553
- exit 130
554
- }
555
-
556
- # Install the cleanup trap BEFORE creating the probe (and before any operation
557
- # that can fail on it) so every exit path — auto-stash failure, tasks gate
558
- # failure, auto-sync rebase conflict, preflight merge conflict — triggers
559
- # release_transient_worktrees. Previously the trap was installed much later,
560
- # leaving stranded __source-probe-* worktrees in mid-rebase state.
561
- trap cleanup EXIT
562
- trap handle_interrupt INT TERM HUP
563
197
 
564
198
  if [[ -z "$source_worktree" ]]; then
565
199
  source_probe_path="${agent_worktree_root}/__source-probe-${SOURCE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
@@ -569,79 +203,10 @@ if [[ -z "$source_worktree" ]]; then
569
203
  created_source_probe=1
570
204
  fi
571
205
 
572
- if [[ -z "$TIER_LEVEL" && -n "$TIER_LEVEL_RAW" ]]; then
573
- if ! TIER_LEVEL="$(normalize_tier "$TIER_LEVEL_RAW")"; then
574
- exit 1
575
- fi
576
- fi
577
-
578
- if [[ -z "$TIER_LEVEL" ]]; then
579
- TIER_LEVEL="$(resolve_tier_from_manifest "$source_worktree" 2>/dev/null || true)"
580
- fi
581
-
582
- if [[ -z "$TIER_LEVEL" ]]; then
583
- TIER_LEVEL="T3"
584
- fi
585
-
586
- case "$TIER_LEVEL" in
587
- T0|T1)
588
- if [[ "$SKIP_TASKS_GATE" -ne 1 ]]; then
589
- echo "[agent-branch-finish] Tier ${TIER_LEVEL}: skipping OpenSpec tasks gate." >&2
590
- SKIP_TASKS_GATE=1
591
- fi
592
- ;;
593
- esac
594
-
595
- pr_already_merged=0
596
- pr_already_merged_url=""
597
- pr_already_merged_at=""
598
- if [[ "$MERGE_MODE" != "direct" ]] && command -v "$GH_BIN" >/dev/null 2>&1; then
599
- pr_check_payload="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
600
- if [[ -n "$pr_check_payload" ]]; then
601
- pr_check_state=""
602
- pr_check_merged_at=""
603
- pr_check_url=""
604
- IFS=$'\x1f' read -r pr_check_state pr_check_merged_at pr_check_url <<< "$pr_check_payload" || true
605
- if [[ "$pr_check_state" == "MERGED" ]]; then
606
- pr_already_merged=1
607
- pr_already_merged_url="$pr_check_url"
608
- pr_already_merged_at="$pr_check_merged_at"
609
- echo "[agent-branch-finish] PR for '${SOURCE_BRANCH}' is already MERGED on origin${pr_check_url:+ (${pr_check_url})}." >&2
610
- echo "[agent-branch-finish] Skipping pre-merge work (auto-tick, tasks gate, sync, preflight, merge-quality gate, push/merge) and proceeding to cleanup." >&2
611
- fi
612
- fi
613
- fi
614
-
615
- if [[ "$pr_already_merged" -ne 1 && "$SKIP_TASKS_GATE" -ne 1 ]]; then
616
- if [[ "$SOURCE_BRANCH" =~ ^agent/ && "$BASE_BRANCH" != agent/* ]]; then
617
- auto_tick_script="${repo_root}/scripts/openspec/auto-tick-tasks.py"
618
- if [[ -f "$auto_tick_script" ]]; then
619
- python3 "$auto_tick_script" \
620
- --worktree "$source_worktree" \
621
- --branch "$SOURCE_BRANCH" \
622
- --base "$BASE_BRANCH" \
623
- || echo "[agent-branch-finish] auto-tick-tasks helper failed; continuing with gate" >&2
624
- fi
625
- fi
626
- validate_openspec_tasks_gate "$SOURCE_BRANCH" "$source_worktree"
627
- elif [[ "$pr_already_merged" -ne 1 ]]; then
628
- echo "[agent-branch-finish] Skipping OpenSpec tasks gate (--skip-tasks-gate)." >&2
629
- fi
630
-
631
206
  if ! is_clean_worktree "$source_worktree"; then
632
- stash_label="guardex/pre-finish/${SOURCE_BRANCH//\//-}-$(date +%Y%m%d-%H%M%S)"
633
207
  echo "[agent-branch-finish] Source worktree is not clean for '${SOURCE_BRANCH}': ${source_worktree}" >&2
634
- echo "[agent-branch-finish] Auto-stashing uncommitted changes as '${stash_label}' so finish can proceed." >&2
635
- if git -C "$source_worktree" stash push --include-untracked -m "$stash_label" >/dev/null 2>&1; then
636
- echo "[agent-branch-finish] Recover later with: git -C \"$source_worktree\" stash list | grep '${stash_label}'" >&2
637
- else
638
- echo "[agent-branch-finish] Auto-stash failed; commit/stash changes on the source branch before finishing." >&2
639
- exit 1
640
- fi
641
- if ! is_clean_worktree "$source_worktree"; then
642
- echo "[agent-branch-finish] Source worktree still not clean after auto-stash; aborting." >&2
643
- exit 1
644
- fi
208
+ echo "[agent-branch-finish] Commit/stash changes on the source branch before finishing." >&2
209
+ exit 1
645
210
  fi
646
211
 
647
212
  start_ref="$BASE_BRANCH"
@@ -662,103 +227,51 @@ case "$require_before_finish" in
662
227
  *) should_require_sync=1 ;;
663
228
  esac
664
229
 
665
- if [[ "$pr_already_merged" -ne 1 && "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
230
+ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
666
231
  behind_count="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
667
232
  behind_count="${behind_count:-0}"
668
233
  if [[ "$behind_count" -gt 0 ]]; then
669
- pr_merge_state=""
670
- if command -v "$GH_BIN" >/dev/null 2>&1; then
671
- pr_merge_state="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state --jq '.state' 2>/dev/null || true)"
672
- fi
673
-
674
- if [[ "$pr_merge_state" == "MERGED" ]]; then
675
- echo "[agent-sync-guard] Branch '${SOURCE_BRANCH}' is behind origin/${BASE_BRANCH} by ${behind_count} commit(s), but its PR is already MERGED on origin." >&2
676
- echo "[agent-sync-guard] Skipping pre-merge rebase to avoid replaying commits the squash already shipped; proceeding to cleanup." >&2
677
- else
678
- echo "[agent-sync-guard] Branch '${SOURCE_BRANCH}' is behind origin/${BASE_BRANCH} by ${behind_count} commit(s)." >&2
679
- echo "[agent-sync-guard] Auto-syncing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH} before finish..." >&2
680
- if ! git -C "$source_worktree" rebase "origin/${BASE_BRANCH}"; then
681
- git_dir="$(git -C "$source_worktree" rev-parse --git-dir)"
682
- rebase_active=0
683
- if [[ -e "${git_dir}/rebase-merge" || -e "${git_dir}/rebase-apply" ]]; then
684
- rebase_active=1
685
- fi
686
-
687
- echo "[agent-sync-guard] Auto-sync failed while rebasing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH}." >&2
688
- if [[ "$rebase_active" -eq 1 ]]; then
689
- echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
690
- echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
691
- fi
692
- exit 1
234
+ echo "[agent-sync-guard] Branch '${SOURCE_BRANCH}' is behind origin/${BASE_BRANCH} by ${behind_count} commit(s)." >&2
235
+ echo "[agent-sync-guard] Auto-syncing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH} before finish..." >&2
236
+ if ! git -C "$source_worktree" rebase "origin/${BASE_BRANCH}"; then
237
+ git_dir="$(git -C "$source_worktree" rev-parse --git-dir)"
238
+ rebase_active=0
239
+ if [[ -e "${git_dir}/rebase-merge" || -e "${git_dir}/rebase-apply" ]]; then
240
+ rebase_active=1
693
241
  fi
694
242
 
695
- behind_after="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
696
- behind_after="${behind_after:-0}"
697
- echo "[agent-sync-guard] Auto-sync complete (behind now: ${behind_after})." >&2
243
+ echo "[agent-sync-guard] Auto-sync failed while rebasing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH}." >&2
244
+ if [[ "$rebase_active" -eq 1 ]]; then
245
+ echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
246
+ echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
247
+ fi
248
+ exit 1
698
249
  fi
699
- fi
700
- fi
701
-
702
- integration_worktree=""
703
- integration_branch=""
704
- use_integration_worktree=1
705
- if [[ "$pr_already_merged" -eq 1 ]]; then
706
- # Pre-merged PR: cleanup-only path; no integration worktree needed.
707
- use_integration_worktree=0
708
- elif [[ "$MERGE_MODE" == "pr" && "$PUSH_ENABLED" -eq 1 ]]; then
709
- # PR mode merges by pushing the source branch and letting GitHub merge.
710
- # Skip creating temporary local integration worktrees in this lane.
711
- use_integration_worktree=0
712
- fi
713
250
 
714
- if [[ "$use_integration_worktree" -eq 1 ]]; then
715
- integration_worktree="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
716
- integration_branch="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
717
- mkdir -p "$(dirname "$integration_worktree")"
718
- git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
719
- git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null
720
- else
721
- integration_worktree=""
722
- integration_branch=""
251
+ behind_after="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
252
+ behind_after="${behind_after:-0}"
253
+ echo "[agent-sync-guard] Auto-sync complete (behind now: ${behind_after})." >&2
254
+ fi
723
255
  fi
724
256
 
725
- merge_gate_json=""
726
-
727
- run_merge_quality_gate() {
728
- if [[ ! -x "${repo_root}/scripts/omx-merge-gate.sh" ]]; then
729
- echo "[agent-branch-finish] Required merge-gate helper is missing: scripts/omx-merge-gate.sh" >&2
730
- echo "[agent-branch-finish] Repair with: gx doctor (or restore the script) before finishing." >&2
731
- return 1
732
- fi
257
+ integration_worktree="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
258
+ integration_branch="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
259
+ mkdir -p "$(dirname "$integration_worktree")"
733
260
 
734
- local gate_args=(--branch "$SOURCE_BRANCH" --base "$BASE_BRANCH" --output-dir "${repo_root}/.omx/state/merge-gates")
735
- if [[ -n "$PR_REF" ]]; then
736
- gate_args+=(--pr "$PR_REF")
737
- fi
738
- if [[ -n "$GH_REPO_REF" ]]; then
739
- gate_args+=(--repo "$GH_REPO_REF")
740
- fi
741
- if [[ "$REQUIRE_REMOTE_GATES" -eq 1 ]]; then
742
- gate_args+=(--require-remote)
743
- fi
261
+ git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
262
+ git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null
744
263
 
745
- local gate_output=""
746
- if gate_output="$(bash "${repo_root}/scripts/omx-merge-gate.sh" "${gate_args[@]}" 2>&1)"; then
747
- printf '%s\n' "$gate_output"
748
- merge_gate_json="$(printf '%s\n' "$gate_output" | sed -n 's/^Merge gate JSON: //p' | tail -n1)"
749
- return 0
264
+ cleanup() {
265
+ if [[ -d "$integration_worktree" ]]; then
266
+ git -C "$repo_root" worktree remove "$integration_worktree" --force >/dev/null 2>&1 || true
750
267
  fi
751
-
752
- merge_gate_json="$(printf '%s\n' "$gate_output" | sed -n 's/^Merge gate JSON: //p' | tail -n1)"
753
- echo "$gate_output" >&2
754
- echo "[agent-branch-finish] Merge-quality gate failed. Resolve blockers before finishing." >&2
755
- if [[ -n "$merge_gate_json" ]]; then
756
- echo "[agent-branch-finish] Gate details: ${merge_gate_json}" >&2
268
+ if [[ "$created_source_probe" -eq 1 && -n "$source_probe_path" && -d "$source_probe_path" ]]; then
269
+ git -C "$repo_root" worktree remove "$source_probe_path" --force >/dev/null 2>&1 || true
757
270
  fi
758
- return 1
759
271
  }
272
+ trap cleanup EXIT
760
273
 
761
- if [[ "$pr_already_merged" -ne 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
274
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
762
275
  git -C "$source_worktree" fetch origin "$BASE_BRANCH" --quiet
763
276
 
764
277
  if ! git -C "$source_worktree" merge --no-commit --no-ff "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
@@ -779,29 +292,16 @@ if [[ "$pr_already_merged" -ne 1 ]] && git -C "$repo_root" show-ref --verify --q
779
292
  git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
780
293
  fi
781
294
 
782
- if [[ "$pr_already_merged" -ne 1 ]] && ! run_merge_quality_gate; then
295
+ if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
296
+ echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
297
+ git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
783
298
  exit 1
784
299
  fi
785
300
 
786
- if [[ "$pr_already_merged" -ne 1 && "$use_integration_worktree" -eq 1 ]]; then
787
- if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
788
- echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
789
- git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
790
- exit 1
791
- fi
792
- fi
793
-
794
- if [[ "$pr_already_merged" -eq 1 ]]; then
795
- merge_completed=1
796
- merge_status="pr-already-merged"
797
- direct_push_error=""
798
- pr_url="$pr_already_merged_url"
799
- else
800
- merge_completed=1
801
- merge_status="direct"
802
- direct_push_error=""
803
- pr_url=""
804
- fi
301
+ merge_completed=1
302
+ merge_status="direct"
303
+ direct_push_error=""
304
+ pr_url=""
805
305
 
806
306
  is_local_branch_delete_error() {
807
307
  local output="$1"
@@ -814,90 +314,26 @@ is_local_branch_delete_error() {
814
314
  return 1
815
315
  }
816
316
 
817
- delete_local_source_branch() {
818
- local branch="$1"
819
- local base_branch="$2"
820
- local delete_output=""
821
- local branch_upstream=""
822
- local safe_delete_ref=""
823
- local safe_to_force_delete=0
824
-
825
- branch_upstream="$(git -C "$repo_root" for-each-ref --count=1 --format='%(upstream:short)' "refs/heads/${branch}" || true)"
826
- branch_upstream="${branch_upstream:-}"
827
- if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
828
- safe_delete_ref="origin/${base_branch}"
829
- elif git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
830
- safe_delete_ref="${base_branch}"
831
- fi
832
- if [[ -n "$safe_delete_ref" ]] && git -C "$repo_root" merge-base --is-ancestor "$branch" "$safe_delete_ref" >/dev/null 2>&1; then
833
- safe_to_force_delete=1
834
- fi
835
-
836
- if delete_output="$(git -C "$repo_root" branch -d "$branch" 2>&1)"; then
837
- return 0
838
- fi
839
-
840
- if [[ "$branch_upstream" == "origin/${branch}" ]]; then
841
- git -C "$repo_root" branch --unset-upstream "$branch" >/dev/null 2>&1 || true
842
- if git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
843
- echo "[agent-branch-finish] Cleared upstream tracking for '${branch}' to complete local merged-branch cleanup." >&2
844
- return 0
845
- fi
846
- fi
847
-
848
- if [[ "$safe_to_force_delete" -eq 1 ]]; then
849
- if git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1; then
850
- echo "[agent-branch-finish] Deleted '${branch}' with forced local cleanup after verifying merge ancestry in '${safe_delete_ref}'." >&2
851
- return 0
852
- fi
853
- fi
854
-
855
- echo "[agent-branch-finish] Failed to delete local branch '${branch}' after merge." >&2
856
- echo "$delete_output" >&2
857
- return 1
858
- }
859
-
860
317
  read_pr_state() {
861
- local preferred_ref="${1:-}"
862
- local state_line=""
863
- local refs_to_try=()
864
- local candidate_ref
865
-
866
- if [[ -n "$preferred_ref" ]]; then
867
- refs_to_try+=("$preferred_ref")
868
- fi
869
- if [[ -n "$pr_url" && "$pr_url" != "$preferred_ref" ]]; then
870
- refs_to_try+=("$pr_url")
871
- fi
872
- if [[ "$SOURCE_BRANCH" != "$preferred_ref" ]]; then
873
- refs_to_try+=("$SOURCE_BRANCH")
318
+ local state_line
319
+ state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
320
+ if [[ -z "$state_line" ]]; then
321
+ return 1
874
322
  fi
875
323
 
876
- for candidate_ref in "${refs_to_try[@]}"; do
877
- state_line="$("$GH_BIN" pr view "$candidate_ref" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
878
- if [[ -z "$state_line" ]]; then
879
- continue
880
- fi
881
-
882
- local parsed_state=""
883
- local parsed_merged_at=""
884
- local parsed_url=""
885
- IFS=$'\x1f' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
886
- PR_STATE="$parsed_state"
887
- PR_MERGED_AT="$parsed_merged_at"
888
- if [[ -n "$parsed_url" ]]; then
889
- pr_url="$parsed_url"
890
- fi
891
- return 0
892
- done
893
-
894
- return 1
324
+ local parsed_state=""
325
+ local parsed_merged_at=""
326
+ local parsed_url=""
327
+ IFS=$'\x1f' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
328
+ PR_STATE="$parsed_state"
329
+ PR_MERGED_AT="$parsed_merged_at"
330
+ if [[ -n "$parsed_url" ]]; then
331
+ pr_url="$parsed_url"
332
+ fi
333
+ return 0
895
334
  }
896
335
 
897
336
  wait_for_pr_merge() {
898
- # Integration/source-probe worktrees are no longer needed during GH check wait loops.
899
- # Release them early so long waits do not leave temporary repos visible in Source Control.
900
- release_transient_worktrees
901
337
  local deadline
902
338
  deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
903
339
  local wait_notice_printed=0
@@ -980,13 +416,6 @@ run_pr_flow() {
980
416
  echo "[agent-branch-finish] PR merged but gh could not delete the local branch (active worktree); continuing local cleanup." >&2
981
417
  return 0
982
418
  fi
983
- PR_STATE=""
984
- PR_MERGED_AT=""
985
- if read_pr_state "$pr_url"; then
986
- if [[ "$PR_STATE" == "MERGED" || -n "$PR_MERGED_AT" ]]; then
987
- return 0
988
- fi
989
- fi
990
419
 
991
420
  if [[ "$WAIT_FOR_MERGE" -eq 1 ]]; then
992
421
  wait_for_pr_merge
@@ -1009,43 +438,7 @@ run_pr_flow() {
1009
438
  return 2
1010
439
  }
1011
440
 
1012
- capture_post_merge_learning() {
1013
- if [[ ! -x "${repo_root}/scripts/omx-learning-capture.sh" ]]; then
1014
- return 0
1015
- fi
1016
-
1017
- local learning_args=(
1018
- --branch "$SOURCE_BRANCH"
1019
- --base "$BASE_BRANCH"
1020
- --outcome "merged-${merge_status}"
1021
- --summary "Merged ${SOURCE_BRANCH} into ${BASE_BRANCH} via ${merge_status} flow."
1022
- --output-dir "${repo_root}/.omx/learning"
1023
- )
1024
- if [[ -n "$PR_REF" ]]; then
1025
- learning_args+=(--pr "$PR_REF")
1026
- elif [[ -n "$pr_url" ]]; then
1027
- learning_args+=(--pr "$pr_url")
1028
- fi
1029
- if [[ -n "$GH_REPO_REF" ]]; then
1030
- learning_args+=(--repo "$GH_REPO_REF")
1031
- fi
1032
- if [[ -n "$merge_gate_json" ]]; then
1033
- learning_args+=(--merge-gate-file "$merge_gate_json")
1034
- fi
1035
- if [[ -f "${source_worktree}/.omx/context/github/sandbox-startup-latest.json" ]]; then
1036
- learning_args+=(--context-file "${source_worktree}/.omx/context/github/sandbox-startup-latest.json")
1037
- fi
1038
-
1039
- local learning_output=""
1040
- if learning_output="$(bash "${repo_root}/scripts/omx-learning-capture.sh" "${learning_args[@]}" 2>&1)"; then
1041
- printf '%s\n' "$learning_output"
1042
- else
1043
- echo "[agent-branch-finish] Warning: post-merge learning capture failed." >&2
1044
- printf '%s\n' "$learning_output" >&2
1045
- fi
1046
- }
1047
-
1048
- if [[ "$pr_already_merged" -ne 1 && "$PUSH_ENABLED" -eq 1 ]]; then
441
+ if [[ "$PUSH_ENABLED" -eq 1 ]]; then
1049
442
  if [[ "$MERGE_MODE" != "pr" ]]; then
1050
443
  if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
1051
444
  direct_push_error="$direct_push_output"
@@ -1091,10 +484,6 @@ if [[ "$pr_already_merged" -ne 1 && "$PUSH_ENABLED" -eq 1 ]]; then
1091
484
  fi
1092
485
  fi
1093
486
 
1094
- if [[ "$merge_completed" -eq 1 ]]; then
1095
- capture_post_merge_learning
1096
- fi
1097
-
1098
487
  if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
1099
488
  python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
1100
489
  fi
@@ -1105,9 +494,6 @@ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_
1105
494
  fi
1106
495
 
1107
496
  if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
1108
- cleanup_incomplete=0
1109
- cleanup_remaining_messages=()
1110
-
1111
497
  if [[ "$source_worktree" == "$repo_root" ]]; then
1112
498
  if is_clean_worktree "$source_worktree"; then
1113
499
  switched_to_base=0
@@ -1128,7 +514,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
1128
514
  git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
1129
515
  fi
1130
516
 
1131
- delete_local_source_branch "$SOURCE_BRANCH" "$BASE_BRANCH"
517
+ git -C "$repo_root" branch -d "$SOURCE_BRANCH"
1132
518
 
1133
519
  if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
1134
520
  if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
@@ -1137,46 +523,28 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
1137
523
  fi
1138
524
 
1139
525
  if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
1140
- prune_args=(--base "$BASE_BRANCH" --branch "$SOURCE_BRANCH" --only-dirty-worktrees --delete-branches --force-merged)
526
+ prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
1141
527
  if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
1142
528
  prune_args+=(--delete-remote-branches)
1143
529
  fi
1144
530
  if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
1145
531
  echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
1146
- echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches" >&2
1147
- fi
1148
- fi
1149
-
1150
- if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${SOURCE_BRANCH}"; then
1151
- cleanup_incomplete=1
1152
- cleanup_remaining_messages+=("local branch still exists: ${SOURCE_BRANCH}")
1153
- fi
1154
-
1155
- if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
1156
- if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
1157
- cleanup_incomplete=1
1158
- cleanup_remaining_messages+=("remote branch still exists: origin/${SOURCE_BRANCH}")
532
+ echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
1159
533
  fi
1160
534
  fi
1161
535
 
1162
- if [[ "$source_worktree" == "${agent_worktree_root}"/* && -d "$source_worktree" ]]; then
1163
- cleanup_incomplete=1
1164
- cleanup_remaining_messages+=("agent worktree path still exists: ${source_worktree}")
536
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
537
+ if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
538
+ echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
539
+ echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
1165
540
  fi
1166
-
1167
- if [[ "$cleanup_incomplete" -eq 1 ]]; then
1168
- echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow, but mandatory cleanup is still incomplete." >&2
1169
- for cleanup_message in "${cleanup_remaining_messages[@]}"; do
1170
- echo "[agent-branch-finish] Remaining cleanup: ${cleanup_message}" >&2
1171
- done
1172
- if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
1173
- echo "[agent-branch-finish] Leave this active sandbox directory, then rerun: bash scripts/agent-branch-finish.sh --branch ${SOURCE_BRANCH} --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup" >&2
541
+ else
542
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
543
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
544
+ echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
1174
545
  fi
1175
- exit 1
1176
546
  fi
1177
547
 
1178
- echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
1179
- else
1180
548
  echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and kept source branch/worktree."
1181
549
  echo "[agent-branch-finish] Cleanup later with: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches"
1182
550
  fi