@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
|
@@ -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>] [--
|
|
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]
|
|
635
|
-
|
|
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 [[ "$
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
746
|
-
if
|
|
747
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" --
|
|
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
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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 [[ "$
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|