@imdeadpool/guardex 7.0.15 → 7.0.18

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.15",
4
- "description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
3
+ "version": "7.0.18",
4
+ "description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
7
7
  "bin": {
@@ -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",
@@ -7,22 +7,22 @@
7
7
  `GUARDEX_ON=0` disables Guardex for that repo.
8
8
  `GUARDEX_ON=1` explicitly enables Guardex for that repo again.
9
9
 
10
- **Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `scripts/agent-branch-start.sh "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
10
+ **Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `gx branch start "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
11
11
  For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
12
12
  Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
13
13
 
14
- **Ownership.** Before editing, claim files: `scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
14
+ **Ownership.** Before editing, claim files: `gx locks claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
15
15
 
16
16
  **Handoff gate.** Post a one-line handoff note (plan/change, owned scope, intended action) before editing. Re-read the latest handoffs before replacing others' code.
17
17
 
18
- **Completion.** Finish with `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup` (or `gx finish --all`). Task is only complete when: commit pushed, PR URL recorded, state = `MERGED`, sandbox worktree pruned. If anything blocks, append a `BLOCKED:` note and stop - don't half-finish.
18
+ **Completion.** Finish with `gx branch finish --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup` (or `gx finish --all`). Task is only complete when: commit pushed, PR URL recorded, state = `MERGED`, sandbox worktree pruned. If anything blocks, append a `BLOCKED:` note and stop - don't half-finish.
19
19
  OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR before considering the branch complete.
20
20
 
21
21
  **Parallel safety.** Assume other agents edit nearby. Never revert unrelated changes. Report conflicts in the handoff.
22
22
 
23
23
  **Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
24
24
 
25
- **OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `scripts/agent-branch-finish.sh ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
25
+ **OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `gx branch finish ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
26
26
 
27
27
  **Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
28
28
  <!-- multiagent-safety:END -->
@@ -64,7 +64,7 @@ echo "[agent-primary-branch-guard] Primary checkout switched branches." >&2
64
64
  echo "[agent-primary-branch-guard] from: $prev_branch (protected)" >&2
65
65
  echo "[agent-primary-branch-guard] to: $new_branch" >&2
66
66
  echo "[agent-primary-branch-guard] The primary working tree must stay on its base/protected branch." >&2
67
- echo "[agent-primary-branch-guard] Use 'git worktree add' (or scripts/agent-branch-start.sh) for feature work." >&2
67
+ echo "[agent-primary-branch-guard] Use 'git worktree add' (or gx branch start) for feature work." >&2
68
68
 
69
69
  if [[ "$is_agent" == "1" ]]; then
70
70
  echo "[agent-primary-branch-guard] Agent session detected — reverting to '$prev_branch'." >&2
@@ -32,17 +32,30 @@ if [[ "$branch" != "$base_branch" ]]; then
32
32
  exit 0
33
33
  fi
34
34
 
35
- cli_path="$repo_root/bin/multiagent-safety.js"
36
- if [[ ! -f "$cli_path" ]]; then
35
+ if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then
36
+ node_bin="${GUARDEX_NODE_BIN:-node}"
37
+ if command -v "$node_bin" >/dev/null 2>&1; then
38
+ "$node_bin" "$GUARDEX_CLI_ENTRY" cleanup \
39
+ --target "$repo_root" \
40
+ --base "$base_branch" \
41
+ --include-pr-merged \
42
+ --keep-clean-worktrees >/dev/null 2>&1 || true
43
+ fi
37
44
  exit 0
38
45
  fi
39
46
 
40
- node_bin="${GUARDEX_NODE_BIN:-node}"
41
- if ! command -v "$node_bin" >/dev/null 2>&1; then
42
- exit 0
47
+ cli_bin="${GUARDEX_CLI_BIN:-}"
48
+ if [[ -z "$cli_bin" ]]; then
49
+ if command -v gx >/dev/null 2>&1; then
50
+ cli_bin="gx"
51
+ elif command -v gitguardex >/dev/null 2>&1; then
52
+ cli_bin="gitguardex"
53
+ else
54
+ exit 0
55
+ fi
43
56
  fi
44
57
 
45
- "$node_bin" "$cli_path" cleanup \
58
+ "$cli_bin" cleanup \
46
59
  --target "$repo_root" \
47
60
  --base "$base_branch" \
48
61
  --include-pr-merged \
@@ -116,11 +116,11 @@ if [[ "$should_require_codex_agent_branch" == "1" && "${GUARDEX_ALLOW_CODEX_ON_N
116
116
 
117
117
  cat >&2 <<'MSG'
118
118
  [guardex-preedit-guard] Codex edit/commit detected on a protected branch.
119
- GuardeX requires Codex work to run from an isolated agent/* branch.
119
+ GitGuardex requires Codex work to run from an isolated agent/* branch.
120
120
  Start the sub-branch/worktree with:
121
- bash scripts/codex-agent.sh "<task-or-plan>" "<agent-name>"
121
+ gx branch start "<task-or-plan>" "<agent-name>"
122
122
  Or manually:
123
- bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
123
+ gx branch start "<task-or-plan>" "<agent-name>"
124
124
  Then commit from the created agent/* branch.
125
125
 
126
126
  Temporary bypass (not recommended):
@@ -132,7 +132,7 @@ MSG
132
132
  cat >&2 <<'MSG'
133
133
  [codex-branch-guard] Codex agent commit blocked on non-agent branch.
134
134
  Use isolated branch/worktree first:
135
- bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
135
+ gx branch start "<task-or-plan>" "<agent-name>"
136
136
  Then commit from the created agent/* branch.
137
137
 
138
138
  Temporary bypass (not recommended):
@@ -163,9 +163,9 @@ if [[ "$is_protected_branch" == "1" ]]; then
163
163
  cat >&2 <<'MSG'
164
164
  [agent-branch-guard] Direct commits on protected branches are blocked.
165
165
  Use an agent branch first:
166
- bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
166
+ gx branch start "<task-or-plan>" "<agent-name>"
167
167
  After finishing work:
168
- bash scripts/agent-branch-finish.sh
168
+ gx branch finish
169
169
 
170
170
  Temporary bypass (not recommended):
171
171
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
@@ -177,7 +177,7 @@ if [[ "$is_agent_session" == "1" && "$branch" != agent/* ]]; then
177
177
  cat >&2 <<'MSG'
178
178
  [agent-branch-guard] Agent commits must run on dedicated agent/* branches.
179
179
  Start an agent branch first:
180
- bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
180
+ gx branch start "<task-or-plan>" "<agent-name>"
181
181
  Then commit on that branch.
182
182
 
183
183
  Temporary bypass (not recommended):
@@ -199,7 +199,7 @@ if [[ "$branch" == agent/* ]]; then
199
199
  cat >&2 <<'MSG'
200
200
  [agent-branch-guard] Agent branch commits require file ownership locks.
201
201
  Claim files first:
202
- python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
202
+ gx locks claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
203
203
  MSG
204
204
  exit 1
205
205
  fi
@@ -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"
@@ -11,6 +11,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
11
11
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
12
12
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
13
13
  OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
14
+ OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
14
15
  PRINT_NAME_ONLY=0
15
16
  POSITIONAL_ARGS=()
16
17
 
@@ -226,13 +227,35 @@ normalize_bool() {
226
227
 
227
228
  OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
228
229
 
230
+ resolve_openspec_masterplan_label() {
231
+ local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
232
+ local label
233
+
234
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
235
+ printf ''
236
+ return 0
237
+ fi
238
+
239
+ label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
240
+ printf '%s' "$label"
241
+ }
242
+
229
243
  resolve_openspec_plan_slug() {
230
244
  local branch_name="$1"
231
- local task_slug="$2"
245
+ local agent_slug="$2"
246
+ local task_slug="$3"
247
+ local masterplan_label=""
248
+ local branch_leaf=""
232
249
  if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
233
250
  sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
234
251
  return 0
235
252
  fi
253
+ masterplan_label="$(resolve_openspec_masterplan_label)"
254
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
255
+ branch_leaf="${branch_name#agent/${agent_slug}/}"
256
+ sanitize_slug "agent-${agent_slug}-${masterplan_label}-${branch_leaf}" "$task_slug"
257
+ return 0
258
+ fi
236
259
  sanitize_slug "${branch_name//\//-}" "$task_slug"
237
260
  }
238
261
 
@@ -255,6 +278,22 @@ resolve_openspec_capability_slug() {
255
278
  sanitize_slug "$task_slug" "general-behavior"
256
279
  }
257
280
 
281
+ resolve_worktree_leaf() {
282
+ local branch_name="$1"
283
+ local agent_slug="$2"
284
+ local masterplan_label=""
285
+ local branch_leaf=""
286
+
287
+ masterplan_label="$(resolve_openspec_masterplan_label)"
288
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
289
+ branch_leaf="${branch_name#agent/${agent_slug}/}"
290
+ printf 'agent__%s__%s__%s' "$agent_slug" "$masterplan_label" "$branch_leaf"
291
+ return 0
292
+ fi
293
+
294
+ printf '%s' "${branch_name//\//__}"
295
+ }
296
+
258
297
  has_local_changes() {
259
298
  local root="$1"
260
299
  if ! git -C "$root" diff --quiet; then
@@ -497,8 +536,9 @@ done
497
536
 
498
537
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
499
538
  mkdir -p "$worktree_root"
500
- worktree_path="${worktree_root}/${branch_name//\//__}"
501
- openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
539
+ worktree_leaf="$(resolve_worktree_leaf "$branch_name" "$agent_slug")"
540
+ worktree_path="${worktree_root}/${worktree_leaf}"
541
+ openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$agent_slug" "$task_slug")"
502
542
  openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
503
543
  openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
504
544