@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
|
@@ -4,6 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
BASE_BRANCH=""
|
|
5
5
|
BASE_BRANCH_EXPLICIT=0
|
|
6
6
|
SOURCE_BRANCH=""
|
|
7
|
+
SOURCE_BRANCH_EXPLICIT=0
|
|
7
8
|
PUSH_ENABLED=1
|
|
8
9
|
DELETE_REMOTE_BRANCH=0
|
|
9
10
|
DELETE_REMOTE_BRANCH_EXPLICIT=0
|
|
@@ -13,6 +14,14 @@ CLEANUP_AFTER_MERGE_RAW="${GUARDEX_FINISH_CLEANUP:-false}"
|
|
|
13
14
|
WAIT_FOR_MERGE_RAW="${GUARDEX_FINISH_WAIT_FOR_MERGE:-false}"
|
|
14
15
|
WAIT_TIMEOUT_SECONDS_RAW="${GUARDEX_FINISH_WAIT_TIMEOUT_SECONDS:-1800}"
|
|
15
16
|
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
|
|
16
25
|
|
|
17
26
|
normalize_bool() {
|
|
18
27
|
local raw="${1:-}"
|
|
@@ -44,10 +53,62 @@ normalize_int() {
|
|
|
44
53
|
printf '%s' "$value"
|
|
45
54
|
}
|
|
46
55
|
|
|
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
|
+
|
|
47
104
|
CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
|
|
48
105
|
WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "0")"
|
|
49
106
|
WAIT_TIMEOUT_SECONDS="$(normalize_int "$WAIT_TIMEOUT_SECONDS_RAW" "1800" "30")"
|
|
50
107
|
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=""
|
|
51
112
|
|
|
52
113
|
while [[ $# -gt 0 ]]; do
|
|
53
114
|
case "$1" in
|
|
@@ -58,6 +119,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
58
119
|
;;
|
|
59
120
|
--branch)
|
|
60
121
|
SOURCE_BRANCH="${2:-}"
|
|
122
|
+
SOURCE_BRANCH_EXPLICIT=1
|
|
61
123
|
shift 2
|
|
62
124
|
;;
|
|
63
125
|
--no-push)
|
|
@@ -80,6 +142,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
80
142
|
;;
|
|
81
143
|
--no-cleanup)
|
|
82
144
|
CLEANUP_AFTER_MERGE=0
|
|
145
|
+
NO_CLEANUP_REQUESTED=1
|
|
83
146
|
shift
|
|
84
147
|
;;
|
|
85
148
|
--wait-for-merge)
|
|
@@ -102,6 +165,22 @@ while [[ $# -gt 0 ]]; do
|
|
|
102
165
|
MERGE_MODE="${2:-auto}"
|
|
103
166
|
shift 2
|
|
104
167
|
;;
|
|
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
|
+
;;
|
|
105
184
|
--via-pr)
|
|
106
185
|
MERGE_MODE="pr"
|
|
107
186
|
shift
|
|
@@ -110,18 +189,23 @@ while [[ $# -gt 0 ]]; do
|
|
|
110
189
|
MERGE_MODE="direct"
|
|
111
190
|
shift
|
|
112
191
|
;;
|
|
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
|
+
;;
|
|
113
201
|
*)
|
|
114
202
|
echo "[agent-branch-finish] Unknown argument: $1" >&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
|
|
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
|
|
116
204
|
exit 1
|
|
117
205
|
;;
|
|
118
206
|
esac
|
|
119
207
|
done
|
|
120
208
|
|
|
121
|
-
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
122
|
-
DELETE_REMOTE_BRANCH=1
|
|
123
|
-
fi
|
|
124
|
-
|
|
125
209
|
case "$MERGE_MODE" in
|
|
126
210
|
auto|direct|pr) ;;
|
|
127
211
|
*)
|
|
@@ -146,10 +230,65 @@ fi
|
|
|
146
230
|
repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
|
|
147
231
|
agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
|
|
148
232
|
|
|
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
|
+
|
|
149
256
|
if [[ -z "$SOURCE_BRANCH" ]]; then
|
|
150
257
|
SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
151
258
|
fi
|
|
152
259
|
|
|
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
|
+
|
|
153
292
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
154
293
|
echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
|
|
155
294
|
exit 1
|
|
@@ -162,6 +301,28 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
|
162
301
|
fi
|
|
163
302
|
fi
|
|
164
303
|
|
|
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
|
+
|
|
165
326
|
if [[ -z "$BASE_BRANCH" ]]; then
|
|
166
327
|
BASE_BRANCH="dev"
|
|
167
328
|
fi
|
|
@@ -172,6 +333,32 @@ if [[ "$SOURCE_BRANCH" == "$BASE_BRANCH" ]]; then
|
|
|
172
333
|
exit 1
|
|
173
334
|
fi
|
|
174
335
|
|
|
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
|
+
|
|
175
362
|
if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${SOURCE_BRANCH}"; then
|
|
176
363
|
echo "[agent-branch-finish] Local source branch does not exist: ${SOURCE_BRANCH}" >&2
|
|
177
364
|
exit 1
|
|
@@ -191,9 +378,188 @@ is_clean_worktree() {
|
|
|
191
378
|
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
|
|
192
379
|
}
|
|
193
380
|
|
|
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
|
+
|
|
194
510
|
source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
|
|
195
511
|
created_source_probe=0
|
|
196
512
|
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
|
|
197
563
|
|
|
198
564
|
if [[ -z "$source_worktree" ]]; then
|
|
199
565
|
source_probe_path="${agent_worktree_root}/__source-probe-${SOURCE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
|
|
@@ -203,10 +569,79 @@ if [[ -z "$source_worktree" ]]; then
|
|
|
203
569
|
created_source_probe=1
|
|
204
570
|
fi
|
|
205
571
|
|
|
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
|
+
|
|
206
631
|
if ! is_clean_worktree "$source_worktree"; then
|
|
632
|
+
stash_label="guardex/pre-finish/${SOURCE_BRANCH//\//-}-$(date +%Y%m%d-%H%M%S)"
|
|
207
633
|
echo "[agent-branch-finish] Source worktree is not clean for '${SOURCE_BRANCH}': ${source_worktree}" >&2
|
|
208
|
-
echo "[agent-branch-finish]
|
|
209
|
-
|
|
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
|
|
210
645
|
fi
|
|
211
646
|
|
|
212
647
|
start_ref="$BASE_BRANCH"
|
|
@@ -227,51 +662,103 @@ case "$require_before_finish" in
|
|
|
227
662
|
*) should_require_sync=1 ;;
|
|
228
663
|
esac
|
|
229
664
|
|
|
230
|
-
if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
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
|
|
231
666
|
behind_count="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
|
|
232
667
|
behind_count="${behind_count:-0}"
|
|
233
668
|
if [[ "$behind_count" -gt 0 ]]; then
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
686
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
247
693
|
fi
|
|
248
|
-
exit 1
|
|
249
|
-
fi
|
|
250
694
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
698
|
+
fi
|
|
254
699
|
fi
|
|
255
700
|
fi
|
|
256
701
|
|
|
257
|
-
integration_worktree="
|
|
258
|
-
integration_branch="
|
|
259
|
-
|
|
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
|
+
|
|
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=""
|
|
723
|
+
fi
|
|
724
|
+
|
|
725
|
+
merge_gate_json=""
|
|
260
726
|
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
263
733
|
|
|
264
|
-
|
|
265
|
-
if [[ -
|
|
266
|
-
|
|
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")
|
|
267
737
|
fi
|
|
268
|
-
if [[
|
|
269
|
-
|
|
738
|
+
if [[ -n "$GH_REPO_REF" ]]; then
|
|
739
|
+
gate_args+=(--repo "$GH_REPO_REF")
|
|
270
740
|
fi
|
|
741
|
+
if [[ "$REQUIRE_REMOTE_GATES" -eq 1 ]]; then
|
|
742
|
+
gate_args+=(--require-remote)
|
|
743
|
+
fi
|
|
744
|
+
|
|
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
|
|
750
|
+
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
|
|
757
|
+
fi
|
|
758
|
+
return 1
|
|
271
759
|
}
|
|
272
|
-
trap cleanup EXIT
|
|
273
760
|
|
|
274
|
-
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
761
|
+
if [[ "$pr_already_merged" -ne 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
275
762
|
git -C "$source_worktree" fetch origin "$BASE_BRANCH" --quiet
|
|
276
763
|
|
|
277
764
|
if ! git -C "$source_worktree" merge --no-commit --no-ff "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
|
|
@@ -292,16 +779,29 @@ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRA
|
|
|
292
779
|
git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
|
|
293
780
|
fi
|
|
294
781
|
|
|
295
|
-
if
|
|
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
|
|
782
|
+
if [[ "$pr_already_merged" -ne 1 ]] && ! run_merge_quality_gate; then
|
|
298
783
|
exit 1
|
|
299
784
|
fi
|
|
300
785
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
305
805
|
|
|
306
806
|
is_local_branch_delete_error() {
|
|
307
807
|
local output="$1"
|
|
@@ -314,26 +814,90 @@ is_local_branch_delete_error() {
|
|
|
314
814
|
return 1
|
|
315
815
|
}
|
|
316
816
|
|
|
317
|
-
|
|
318
|
-
local
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
322
846
|
fi
|
|
323
847
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
PR_MERGED_AT="$parsed_merged_at"
|
|
330
|
-
if [[ -n "$parsed_url" ]]; then
|
|
331
|
-
pr_url="$parsed_url"
|
|
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
|
|
332
853
|
fi
|
|
333
|
-
|
|
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
|
+
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")
|
|
874
|
+
fi
|
|
875
|
+
|
|
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
|
|
334
895
|
}
|
|
335
896
|
|
|
336
897
|
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
|
|
337
901
|
local deadline
|
|
338
902
|
deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
|
|
339
903
|
local wait_notice_printed=0
|
|
@@ -416,6 +980,13 @@ run_pr_flow() {
|
|
|
416
980
|
echo "[agent-branch-finish] PR merged but gh could not delete the local branch (active worktree); continuing local cleanup." >&2
|
|
417
981
|
return 0
|
|
418
982
|
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
|
|
419
990
|
|
|
420
991
|
if [[ "$WAIT_FOR_MERGE" -eq 1 ]]; then
|
|
421
992
|
wait_for_pr_merge
|
|
@@ -438,7 +1009,43 @@ run_pr_flow() {
|
|
|
438
1009
|
return 2
|
|
439
1010
|
}
|
|
440
1011
|
|
|
441
|
-
|
|
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
|
|
442
1049
|
if [[ "$MERGE_MODE" != "pr" ]]; then
|
|
443
1050
|
if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
|
|
444
1051
|
direct_push_error="$direct_push_output"
|
|
@@ -484,6 +1091,10 @@ if [[ "$PUSH_ENABLED" -eq 1 ]]; then
|
|
|
484
1091
|
fi
|
|
485
1092
|
fi
|
|
486
1093
|
|
|
1094
|
+
if [[ "$merge_completed" -eq 1 ]]; then
|
|
1095
|
+
capture_post_merge_learning
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
487
1098
|
if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
|
|
488
1099
|
python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
|
|
489
1100
|
fi
|
|
@@ -494,6 +1105,9 @@ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_
|
|
|
494
1105
|
fi
|
|
495
1106
|
|
|
496
1107
|
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
1108
|
+
cleanup_incomplete=0
|
|
1109
|
+
cleanup_remaining_messages=()
|
|
1110
|
+
|
|
497
1111
|
if [[ "$source_worktree" == "$repo_root" ]]; then
|
|
498
1112
|
if is_clean_worktree "$source_worktree"; then
|
|
499
1113
|
switched_to_base=0
|
|
@@ -514,7 +1128,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
|
514
1128
|
git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
|
|
515
1129
|
fi
|
|
516
1130
|
|
|
517
|
-
|
|
1131
|
+
delete_local_source_branch "$SOURCE_BRANCH" "$BASE_BRANCH"
|
|
518
1132
|
|
|
519
1133
|
if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
520
1134
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
|
|
@@ -523,28 +1137,46 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
|
523
1137
|
fi
|
|
524
1138
|
|
|
525
1139
|
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
526
|
-
prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
|
|
1140
|
+
prune_args=(--base "$BASE_BRANCH" --branch "$SOURCE_BRANCH" --only-dirty-worktrees --delete-branches --force-merged)
|
|
527
1141
|
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
528
1142
|
prune_args+=(--delete-remote-branches)
|
|
529
1143
|
fi
|
|
530
1144
|
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
531
1145
|
echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
|
|
532
|
-
echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&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
|
|
533
1147
|
fi
|
|
534
1148
|
fi
|
|
535
1149
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
|
|
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}")
|
|
540
1153
|
fi
|
|
541
|
-
|
|
542
|
-
if [[ -
|
|
543
|
-
if
|
|
544
|
-
|
|
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}")
|
|
545
1159
|
fi
|
|
546
1160
|
fi
|
|
547
1161
|
|
|
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}")
|
|
1165
|
+
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
|
|
1174
|
+
fi
|
|
1175
|
+
exit 1
|
|
1176
|
+
fi
|
|
1177
|
+
|
|
1178
|
+
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
|
|
1179
|
+
else
|
|
548
1180
|
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and kept source branch/worktree."
|
|
549
1181
|
echo "[agent-branch-finish] Cleanup later with: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches"
|
|
550
1182
|
fi
|