@imdeadpool/guardex 7.0.14 → 7.0.16
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 +37 -4
- package/bin/multiagent-safety.js +1236 -171
- package/package.json +3 -2
- package/templates/scripts/agent-branch-finish.sh +35 -6
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +93 -15
- package/templates/scripts/agent-worktree-prune.sh +78 -44
- package/templates/scripts/codex-agent.sh +96 -4
- package/templates/scripts/guardex-docker-loader.sh +123 -0
- package/templates/scripts/openspec/init-plan-workspace.sh +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.16",
|
|
4
4
|
"description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"agent:codex": "bash ./scripts/codex-agent.sh",
|
|
16
16
|
"agent:branch:start": "bash ./scripts/agent-branch-start.sh",
|
|
17
17
|
"agent:branch:finish": "bash ./scripts/agent-branch-finish.sh",
|
|
18
|
+
"agent:branch:merge": "bash ./scripts/agent-branch-merge.sh",
|
|
18
19
|
"agent:cleanup": "gx cleanup",
|
|
19
20
|
"agent:hooks:install": "bash ./scripts/install-agent-git-hooks.sh",
|
|
20
21
|
"agent:locks:claim": "python3 ./scripts/agent-file-locks.py claim",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"bugs": {
|
|
61
62
|
"url": "https://github.com/recodeee/gitguardex/issues"
|
|
62
63
|
},
|
|
63
|
-
"homepage": "https://
|
|
64
|
+
"homepage": "https://github.com/recodeee/gitguardex-frontend",
|
|
64
65
|
"funding": "https://github.com/sponsors/recodeecom",
|
|
65
66
|
"publishConfig": {
|
|
66
67
|
"access": "public"
|
|
@@ -144,24 +144,44 @@ else
|
|
|
144
144
|
common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
|
|
145
145
|
fi
|
|
146
146
|
repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
|
|
147
|
-
agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
|
|
148
147
|
|
|
149
148
|
if [[ -z "$SOURCE_BRANCH" ]]; then
|
|
150
149
|
SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
151
150
|
fi
|
|
152
151
|
|
|
152
|
+
stored_worktree_root_rel="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexWorktreeRoot" || true)"
|
|
153
|
+
if [[ -z "$stored_worktree_root_rel" ]]; then
|
|
154
|
+
stored_worktree_root_rel=".omx/agent-worktrees"
|
|
155
|
+
fi
|
|
156
|
+
agent_worktree_root="${repo_common_root}/${stored_worktree_root_rel}"
|
|
157
|
+
|
|
153
158
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
154
159
|
echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
|
|
155
160
|
exit 1
|
|
156
161
|
fi
|
|
157
162
|
|
|
158
163
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
159
|
-
|
|
160
|
-
if [[ -n "$
|
|
161
|
-
BASE_BRANCH="$
|
|
164
|
+
source_branch_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexBase" || true)"
|
|
165
|
+
if [[ -n "$source_branch_base" ]]; then
|
|
166
|
+
BASE_BRANCH="$source_branch_base"
|
|
167
|
+
else
|
|
168
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
169
|
+
if [[ -n "$configured_base" ]]; then
|
|
170
|
+
BASE_BRANCH="$configured_base"
|
|
171
|
+
fi
|
|
162
172
|
fi
|
|
163
173
|
fi
|
|
164
174
|
|
|
175
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
176
|
+
for fallback_branch in dev main master; do
|
|
177
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback_branch}" \
|
|
178
|
+
|| git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${fallback_branch}"; then
|
|
179
|
+
BASE_BRANCH="$fallback_branch"
|
|
180
|
+
break
|
|
181
|
+
fi
|
|
182
|
+
done
|
|
183
|
+
fi
|
|
184
|
+
|
|
165
185
|
if [[ -z "$BASE_BRANCH" ]]; then
|
|
166
186
|
BASE_BRANCH="dev"
|
|
167
187
|
fi
|
|
@@ -268,8 +288,17 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
|
|
|
268
288
|
fi
|
|
269
289
|
fi
|
|
270
290
|
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
integration_stamp="$(date +%Y%m%d-%H%M%S)"
|
|
292
|
+
integration_worktree_base="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
|
|
293
|
+
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
|
|
294
|
+
integration_worktree="$integration_worktree_base"
|
|
295
|
+
integration_branch="$integration_branch_base"
|
|
296
|
+
integration_suffix=1
|
|
297
|
+
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
298
|
+
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
299
|
+
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
300
|
+
integration_suffix=$((integration_suffix + 1))
|
|
301
|
+
done
|
|
273
302
|
mkdir -p "$(dirname "$integration_worktree")"
|
|
274
303
|
|
|
275
304
|
git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
BASE_BRANCH=""
|
|
5
|
+
BASE_BRANCH_EXPLICIT=0
|
|
6
|
+
TARGET_BRANCH=""
|
|
7
|
+
TASK_NAME=""
|
|
8
|
+
AGENT_NAME="${GUARDEX_MERGE_AGENT_NAME:-codex}"
|
|
9
|
+
declare -a SOURCE_BRANCHES=()
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Usage: scripts/agent-branch-merge.sh --branch <agent/...> [--branch <agent/...> ...] [--into <agent/...>] [--task <task>] [--agent <agent>] [--base <branch>]
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
bash scripts/agent-branch-merge.sh --branch agent/codex/ui-a --branch agent/codex/ui-b
|
|
17
|
+
bash scripts/agent-branch-merge.sh --into agent/codex/owner-lane --branch agent/codex/helper-a --branch agent/codex/helper-b
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sanitize_slug() {
|
|
22
|
+
local raw="$1"
|
|
23
|
+
local fallback="${2:-merge-agent-branches}"
|
|
24
|
+
local slug
|
|
25
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
26
|
+
if [[ -z "$slug" ]]; then
|
|
27
|
+
slug="$fallback"
|
|
28
|
+
fi
|
|
29
|
+
printf '%s' "$slug"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resolve_base_branch() {
|
|
33
|
+
local repo="$1"
|
|
34
|
+
local explicit_target="$2"
|
|
35
|
+
local configured=""
|
|
36
|
+
local branch_base=""
|
|
37
|
+
|
|
38
|
+
if [[ -n "$explicit_target" ]]; then
|
|
39
|
+
branch_base="$(git -C "$repo" config --get "branch.${explicit_target}.guardexBase" || true)"
|
|
40
|
+
if [[ -n "$branch_base" ]]; then
|
|
41
|
+
printf '%s' "$branch_base"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
configured="$(git -C "$repo" config --get multiagent.baseBranch || true)"
|
|
47
|
+
if [[ -n "$configured" ]]; then
|
|
48
|
+
printf '%s' "$configured"
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
for fallback in dev main master; do
|
|
53
|
+
if git -C "$repo" show-ref --verify --quiet "refs/heads/${fallback}" \
|
|
54
|
+
|| git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/${fallback}"; then
|
|
55
|
+
printf '%s' "$fallback"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
printf '%s' "dev"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get_worktree_for_branch() {
|
|
64
|
+
local repo="$1"
|
|
65
|
+
local branch="$2"
|
|
66
|
+
git -C "$repo" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
|
|
67
|
+
$1 == "worktree" { wt = $2 }
|
|
68
|
+
$1 == "branch" && $2 == target { print wt; exit }
|
|
69
|
+
'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
is_clean_worktree() {
|
|
73
|
+
local wt="$1"
|
|
74
|
+
git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
|
|
75
|
+
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
|
|
76
|
+
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
has_in_progress_git_op() {
|
|
80
|
+
local wt="$1"
|
|
81
|
+
local git_dir=""
|
|
82
|
+
git_dir="$(git -C "$wt" rev-parse --git-dir 2>/dev/null || true)"
|
|
83
|
+
if [[ -z "$git_dir" ]]; then
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
if [[ "$git_dir" != /* ]]; then
|
|
87
|
+
git_dir="$(cd "$wt/$git_dir" 2>/dev/null && pwd -P || true)"
|
|
88
|
+
fi
|
|
89
|
+
if [[ -z "$git_dir" ]]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
[[ -f "${git_dir}/MERGE_HEAD" || -d "${git_dir}/rebase-merge" || -d "${git_dir}/rebase-apply" ]]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
select_unique_worktree_path() {
|
|
96
|
+
local root="$1"
|
|
97
|
+
local name="$2"
|
|
98
|
+
local candidate="${root}/${name}"
|
|
99
|
+
local suffix=2
|
|
100
|
+
while [[ -e "$candidate" ]]; do
|
|
101
|
+
candidate="${root}/${name}-${suffix}"
|
|
102
|
+
suffix=$((suffix + 1))
|
|
103
|
+
done
|
|
104
|
+
printf '%s' "$candidate"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
branch_exists() {
|
|
108
|
+
local repo="$1"
|
|
109
|
+
local branch="$2"
|
|
110
|
+
git -C "$repo" show-ref --verify --quiet "refs/heads/${branch}"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
branch_is_agent_lane() {
|
|
114
|
+
local branch="$1"
|
|
115
|
+
[[ "$branch" == agent/* ]]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
array_contains() {
|
|
119
|
+
local needle="$1"
|
|
120
|
+
shift || true
|
|
121
|
+
local item
|
|
122
|
+
for item in "$@"; do
|
|
123
|
+
if [[ "$item" == "$needle" ]]; then
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
collect_branch_files() {
|
|
131
|
+
local repo="$1"
|
|
132
|
+
local base_ref="$2"
|
|
133
|
+
local branch="$3"
|
|
134
|
+
git -C "$repo" diff --name-only "${base_ref}...${branch}" -- . ":(exclude).omx/state/agent-file-locks.json" 2>/dev/null || true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
while [[ $# -gt 0 ]]; do
|
|
138
|
+
case "$1" in
|
|
139
|
+
--base)
|
|
140
|
+
BASE_BRANCH="${2:-}"
|
|
141
|
+
BASE_BRANCH_EXPLICIT=1
|
|
142
|
+
shift 2
|
|
143
|
+
;;
|
|
144
|
+
--into)
|
|
145
|
+
TARGET_BRANCH="${2:-}"
|
|
146
|
+
shift 2
|
|
147
|
+
;;
|
|
148
|
+
--branch)
|
|
149
|
+
SOURCE_BRANCHES+=("${2:-}")
|
|
150
|
+
shift 2
|
|
151
|
+
;;
|
|
152
|
+
--task)
|
|
153
|
+
TASK_NAME="${2:-}"
|
|
154
|
+
shift 2
|
|
155
|
+
;;
|
|
156
|
+
--agent)
|
|
157
|
+
AGENT_NAME="${2:-codex}"
|
|
158
|
+
shift 2
|
|
159
|
+
;;
|
|
160
|
+
-h|--help)
|
|
161
|
+
usage
|
|
162
|
+
exit 0
|
|
163
|
+
;;
|
|
164
|
+
*)
|
|
165
|
+
echo "[agent-branch-merge] Unknown argument: $1" >&2
|
|
166
|
+
usage >&2
|
|
167
|
+
exit 1
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
173
|
+
echo "[agent-branch-merge] Not inside a git repository." >&2
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
178
|
+
common_git_dir_raw="$(git -C "$repo_root" rev-parse --git-common-dir)"
|
|
179
|
+
if [[ "$common_git_dir_raw" == /* ]]; then
|
|
180
|
+
common_git_dir="$common_git_dir_raw"
|
|
181
|
+
else
|
|
182
|
+
common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
|
|
183
|
+
fi
|
|
184
|
+
repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
|
|
185
|
+
agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
|
|
186
|
+
mkdir -p "$agent_worktree_root"
|
|
187
|
+
|
|
188
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
189
|
+
echo "[agent-branch-merge] --base requires a branch value." >&2
|
|
190
|
+
exit 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
if [[ -z "$TARGET_BRANCH" && "${#SOURCE_BRANCHES[@]}" -lt 1 ]]; then
|
|
194
|
+
echo "[agent-branch-merge] Provide at least one --branch <agent/...> source lane." >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
if [[ -n "$TARGET_BRANCH" ]] && ! branch_is_agent_lane "$TARGET_BRANCH"; then
|
|
199
|
+
echo "[agent-branch-merge] --into must reference an agent/* branch: ${TARGET_BRANCH}" >&2
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
deduped_sources=()
|
|
204
|
+
for branch in "${SOURCE_BRANCHES[@]}"; do
|
|
205
|
+
if [[ -z "$branch" ]]; then
|
|
206
|
+
echo "[agent-branch-merge] --branch requires an agent/* branch value." >&2
|
|
207
|
+
exit 1
|
|
208
|
+
fi
|
|
209
|
+
if ! branch_is_agent_lane "$branch"; then
|
|
210
|
+
echo "[agent-branch-merge] Source branch must be agent/*: ${branch}" >&2
|
|
211
|
+
exit 1
|
|
212
|
+
fi
|
|
213
|
+
if ! branch_exists "$repo_root" "$branch"; then
|
|
214
|
+
echo "[agent-branch-merge] Local source branch not found: ${branch}" >&2
|
|
215
|
+
exit 1
|
|
216
|
+
fi
|
|
217
|
+
if ! array_contains "$branch" "${deduped_sources[@]}"; then
|
|
218
|
+
deduped_sources+=("$branch")
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
SOURCE_BRANCHES=("${deduped_sources[@]}")
|
|
222
|
+
|
|
223
|
+
if [[ "${#SOURCE_BRANCHES[@]}" -eq 0 ]]; then
|
|
224
|
+
echo "[agent-branch-merge] No unique source branches were provided." >&2
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
229
|
+
BASE_BRANCH="$(resolve_base_branch "$repo_root" "$TARGET_BRANCH")"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
233
|
+
echo "[agent-branch-merge] Unable to resolve a base branch." >&2
|
|
234
|
+
exit 1
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
start_ref="$BASE_BRANCH"
|
|
238
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
239
|
+
git -C "$repo_root" fetch origin "$BASE_BRANCH" --quiet
|
|
240
|
+
start_ref="origin/${BASE_BRANCH}"
|
|
241
|
+
elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
|
|
242
|
+
echo "[agent-branch-merge] Base branch not found locally or on origin: ${BASE_BRANCH}" >&2
|
|
243
|
+
exit 1
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
target_worktree=""
|
|
247
|
+
target_created=0
|
|
248
|
+
|
|
249
|
+
if [[ -z "$TARGET_BRANCH" ]]; then
|
|
250
|
+
if [[ -z "$TASK_NAME" ]]; then
|
|
251
|
+
first_hint="$(printf '%s' "${SOURCE_BRANCHES[0]}" | sed -E 's#^agent/[^/]+/##; s#^agent/##')"
|
|
252
|
+
source_count="${#SOURCE_BRANCHES[@]}"
|
|
253
|
+
if [[ "$source_count" -gt 1 ]]; then
|
|
254
|
+
TASK_NAME="$(sanitize_slug "merge-${first_hint}-and-$((source_count - 1))-more" "merge-agent-branches")"
|
|
255
|
+
else
|
|
256
|
+
TASK_NAME="$(sanitize_slug "merge-${first_hint}" "merge-agent-branches")"
|
|
257
|
+
fi
|
|
258
|
+
else
|
|
259
|
+
TASK_NAME="$(sanitize_slug "$TASK_NAME" "merge-agent-branches")"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
start_output=""
|
|
263
|
+
if ! start_output="$(
|
|
264
|
+
cd "$repo_root"
|
|
265
|
+
env GUARDEX_OPENSPEC_AUTO_INIT=1 bash "scripts/agent-branch-start.sh" "$TASK_NAME" "$AGENT_NAME" "$BASE_BRANCH" 2>&1
|
|
266
|
+
)"; then
|
|
267
|
+
printf '%s\n' "$start_output" >&2
|
|
268
|
+
exit 1
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
printf '%s\n' "$start_output"
|
|
272
|
+
TARGET_BRANCH="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Created branch: //p' | head -n 1)"
|
|
273
|
+
target_worktree="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | head -n 1)"
|
|
274
|
+
if [[ -z "$TARGET_BRANCH" || -z "$target_worktree" ]]; then
|
|
275
|
+
echo "[agent-branch-merge] Unable to parse target branch/worktree from agent-branch-start output." >&2
|
|
276
|
+
exit 1
|
|
277
|
+
fi
|
|
278
|
+
target_created=1
|
|
279
|
+
else
|
|
280
|
+
if ! branch_exists "$repo_root" "$TARGET_BRANCH"; then
|
|
281
|
+
echo "[agent-branch-merge] Target branch not found: ${TARGET_BRANCH}" >&2
|
|
282
|
+
exit 1
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
target_worktree="$(get_worktree_for_branch "$repo_root" "$TARGET_BRANCH")"
|
|
286
|
+
if [[ -z "$target_worktree" ]]; then
|
|
287
|
+
target_worktree="$(select_unique_worktree_path "$agent_worktree_root" "${TARGET_BRANCH//\//__}")"
|
|
288
|
+
git -C "$repo_root" worktree add "$target_worktree" "$TARGET_BRANCH" >/dev/null
|
|
289
|
+
target_created=1
|
|
290
|
+
echo "[agent-branch-merge] Attached worktree for target branch '${TARGET_BRANCH}': ${target_worktree}"
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ "$TARGET_BRANCH" == "$BASE_BRANCH" ]]; then
|
|
295
|
+
echo "[agent-branch-merge] Target branch must not equal the protected base branch '${BASE_BRANCH}'." >&2
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if ! is_clean_worktree "$target_worktree"; then
|
|
300
|
+
if [[ "$target_created" -eq 1 ]]; then
|
|
301
|
+
echo "[agent-branch-merge] Target worktree has freshly generated scaffold changes; continuing inside the new integration lane."
|
|
302
|
+
else
|
|
303
|
+
echo "[agent-branch-merge] Target worktree is not clean: ${target_worktree}" >&2
|
|
304
|
+
echo "[agent-branch-merge] Commit, stash, or discard local changes before merging agent lanes." >&2
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if has_in_progress_git_op "$target_worktree"; then
|
|
310
|
+
echo "[agent-branch-merge] Target worktree has an in-progress merge/rebase: ${target_worktree}" >&2
|
|
311
|
+
echo "[agent-branch-merge] Resolve or abort that git operation before running the merge workflow." >&2
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
for source_branch in "${SOURCE_BRANCHES[@]}"; do
|
|
316
|
+
if [[ "$source_branch" == "$TARGET_BRANCH" ]]; then
|
|
317
|
+
echo "[agent-branch-merge] Source branch list includes the target branch: ${source_branch}" >&2
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
source_worktree="$(get_worktree_for_branch "$repo_root" "$source_branch")"
|
|
321
|
+
if [[ -n "$source_worktree" ]] && ! is_clean_worktree "$source_worktree"; then
|
|
322
|
+
echo "[agent-branch-merge] Source worktree is not clean for '${source_branch}': ${source_worktree}" >&2
|
|
323
|
+
echo "[agent-branch-merge] Commit or stash source-lane changes before integration." >&2
|
|
324
|
+
exit 1
|
|
325
|
+
fi
|
|
326
|
+
done
|
|
327
|
+
|
|
328
|
+
pending_branches=()
|
|
329
|
+
for source_branch in "${SOURCE_BRANCHES[@]}"; do
|
|
330
|
+
if git -C "$repo_root" merge-base --is-ancestor "$source_branch" "$TARGET_BRANCH" >/dev/null 2>&1; then
|
|
331
|
+
echo "[agent-branch-merge] Skipping '${source_branch}' because it is already integrated into '${TARGET_BRANCH}'."
|
|
332
|
+
continue
|
|
333
|
+
fi
|
|
334
|
+
pending_branches+=("$source_branch")
|
|
335
|
+
done
|
|
336
|
+
|
|
337
|
+
if [[ "${#pending_branches[@]}" -eq 0 ]]; then
|
|
338
|
+
echo "[agent-branch-merge] No pending source branches remain for target '${TARGET_BRANCH}'."
|
|
339
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}"
|
|
340
|
+
exit 0
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
declare -A file_to_branches=()
|
|
344
|
+
declare -a overlap_files=()
|
|
345
|
+
for source_branch in "${pending_branches[@]}"; do
|
|
346
|
+
while IFS= read -r changed_file; do
|
|
347
|
+
[[ -z "$changed_file" ]] && continue
|
|
348
|
+
existing="${file_to_branches[$changed_file]:-}"
|
|
349
|
+
if [[ -z "$existing" ]]; then
|
|
350
|
+
file_to_branches["$changed_file"]="$source_branch"
|
|
351
|
+
continue
|
|
352
|
+
fi
|
|
353
|
+
if [[ ",${existing}," == *",${source_branch},"* ]]; then
|
|
354
|
+
continue
|
|
355
|
+
fi
|
|
356
|
+
file_to_branches["$changed_file"]="${existing},${source_branch}"
|
|
357
|
+
if ! array_contains "$changed_file" "${overlap_files[@]}"; then
|
|
358
|
+
overlap_files+=("$changed_file")
|
|
359
|
+
fi
|
|
360
|
+
done < <(collect_branch_files "$repo_root" "$start_ref" "$source_branch")
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
echo "[agent-branch-merge] Target branch: ${TARGET_BRANCH}"
|
|
364
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}"
|
|
365
|
+
echo "[agent-branch-merge] Base branch: ${BASE_BRANCH} (${start_ref})"
|
|
366
|
+
echo "[agent-branch-merge] Merge order: ${pending_branches[*]}"
|
|
367
|
+
|
|
368
|
+
if [[ "${#overlap_files[@]}" -gt 0 ]]; then
|
|
369
|
+
echo "[agent-branch-merge] Overlapping changed files detected across requested branches:"
|
|
370
|
+
for overlap_file in "${overlap_files[@]}"; do
|
|
371
|
+
branches_csv="${file_to_branches[$overlap_file]}"
|
|
372
|
+
branches_display="$(printf '%s' "$branches_csv" | sed 's/,/, /g')"
|
|
373
|
+
echo " - ${overlap_file} <- ${branches_display}"
|
|
374
|
+
done
|
|
375
|
+
else
|
|
376
|
+
echo "[agent-branch-merge] No overlapping changed files detected across requested branches."
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
for index in "${!pending_branches[@]}"; do
|
|
380
|
+
source_branch="${pending_branches[$index]}"
|
|
381
|
+
echo "[agent-branch-merge] Merging '${source_branch}' into '${TARGET_BRANCH}'..."
|
|
382
|
+
if git -C "$target_worktree" merge --no-ff --no-edit "$source_branch"; then
|
|
383
|
+
echo "[agent-branch-merge] Merged '${source_branch}'."
|
|
384
|
+
continue
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
conflict_files="$(git -C "$target_worktree" diff --name-only --diff-filter=U || true)"
|
|
388
|
+
echo "[agent-branch-merge] Merge conflict detected while merging '${source_branch}' into '${TARGET_BRANCH}'." >&2
|
|
389
|
+
echo "[agent-branch-merge] Target worktree: ${target_worktree}" >&2
|
|
390
|
+
if [[ -n "$conflict_files" ]]; then
|
|
391
|
+
echo "[agent-branch-merge] Conflicting files:" >&2
|
|
392
|
+
while IFS= read -r conflict_file; do
|
|
393
|
+
[[ -n "$conflict_file" ]] && echo " - ${conflict_file}" >&2
|
|
394
|
+
done <<< "$conflict_files"
|
|
395
|
+
fi
|
|
396
|
+
echo "[agent-branch-merge] Resolve or abort inside the integration worktree:" >&2
|
|
397
|
+
echo " cd \"${target_worktree}\"" >&2
|
|
398
|
+
echo " git status" >&2
|
|
399
|
+
echo " git add <resolved-files> && git commit" >&2
|
|
400
|
+
echo " # or: git merge --abort" >&2
|
|
401
|
+
|
|
402
|
+
remaining_branches=("${pending_branches[@]:$((index + 1))}")
|
|
403
|
+
if [[ "${#remaining_branches[@]}" -gt 0 ]]; then
|
|
404
|
+
echo "[agent-branch-merge] Remaining branches:" >&2
|
|
405
|
+
for remaining in "${remaining_branches[@]}"; do
|
|
406
|
+
echo " - ${remaining}" >&2
|
|
407
|
+
done
|
|
408
|
+
resume_cmd="gx merge --into ${TARGET_BRANCH} --base ${BASE_BRANCH}"
|
|
409
|
+
for remaining in "${remaining_branches[@]}"; do
|
|
410
|
+
resume_cmd="${resume_cmd} --branch ${remaining}"
|
|
411
|
+
done
|
|
412
|
+
echo "[agent-branch-merge] Resume after resolving with: ${resume_cmd}" >&2
|
|
413
|
+
fi
|
|
414
|
+
exit 1
|
|
415
|
+
done
|
|
416
|
+
|
|
417
|
+
echo "[agent-branch-merge] Merge sequence complete for '${TARGET_BRANCH}'."
|
|
418
|
+
if [[ "$target_created" -eq 1 ]]; then
|
|
419
|
+
echo "[agent-branch-merge] Review and verify in '${target_worktree}', then finish the integration branch when ready."
|
|
420
|
+
fi
|
|
421
|
+
echo "[agent-branch-merge] Next step: bash scripts/agent-branch-finish.sh --branch \"${TARGET_BRANCH}\" --base \"${BASE_BRANCH}\" --via-pr --wait-for-merge --cleanup"
|