@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/CONTRIBUTING.md +1 -1
- package/README.md +182 -51
- package/bin/multiagent-safety.js +993 -172
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +4 -4
- package/templates/githooks/post-checkout +1 -1
- package/templates/githooks/post-merge +19 -6
- package/templates/githooks/pre-commit +8 -8
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +43 -3
- package/templates/scripts/agent-session-state.js +110 -0
- package/templates/scripts/codex-agent.sh +124 -2
- package/templates/scripts/install-vscode-active-agents-extension.js +92 -0
- package/templates/scripts/openspec/init-change-workspace.sh +77 -9
- package/templates/scripts/openspec/init-plan-workspace.sh +592 -48
- package/templates/vscode/guardex-active-agents/README.md +21 -0
- package/templates/vscode/guardex-active-agents/extension.js +317 -0
- package/templates/vscode/guardex-active-agents/package.json +57 -0
- package/templates/vscode/guardex-active-agents/session-schema.js +407 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
4
|
-
"description": "
|
|
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 `
|
|
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: `
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
if
|
|
42
|
-
|
|
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
|
-
"$
|
|
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
|
-
|
|
119
|
+
GitGuardex requires Codex work to run from an isolated agent/* branch.
|
|
120
120
|
Start the sub-branch/worktree with:
|
|
121
|
-
|
|
121
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
122
122
|
Or manually:
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
+
gx branch start "<task-or-plan>" "<agent-name>"
|
|
167
167
|
After finishing work:
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
501
|
-
|
|
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
|
|