@imdeadpool/guardex 5.0.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.
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BASE_BRANCH=""
5
+ BASE_BRANCH_EXPLICIT=0
6
+ SOURCE_BRANCH=""
7
+ PUSH_ENABLED=1
8
+ DELETE_REMOTE_BRANCH=1
9
+ MERGE_MODE="auto"
10
+ GH_BIN="${MUSAFETY_GH_BIN:-gh}"
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case "$1" in
14
+ --base)
15
+ BASE_BRANCH="${2:-}"
16
+ BASE_BRANCH_EXPLICIT=1
17
+ shift 2
18
+ ;;
19
+ --branch)
20
+ SOURCE_BRANCH="${2:-}"
21
+ shift 2
22
+ ;;
23
+ --no-push)
24
+ PUSH_ENABLED=0
25
+ shift
26
+ ;;
27
+ --keep-remote-branch)
28
+ DELETE_REMOTE_BRANCH=0
29
+ shift
30
+ ;;
31
+ --mode)
32
+ MERGE_MODE="${2:-auto}"
33
+ shift 2
34
+ ;;
35
+ --via-pr)
36
+ MERGE_MODE="pr"
37
+ shift
38
+ ;;
39
+ --direct-only)
40
+ MERGE_MODE="direct"
41
+ shift
42
+ ;;
43
+ *)
44
+ echo "[agent-branch-finish] Unknown argument: $1" >&2
45
+ echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--keep-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
46
+ exit 1
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ case "$MERGE_MODE" in
52
+ auto|direct|pr) ;;
53
+ *)
54
+ echo "[agent-branch-finish] Invalid --mode value: ${MERGE_MODE} (expected auto|direct|pr)" >&2
55
+ exit 1
56
+ ;;
57
+ esac
58
+
59
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
60
+ echo "[agent-branch-finish] Not inside a git repository." >&2
61
+ exit 1
62
+ fi
63
+
64
+ repo_root="$(git rev-parse --show-toplevel)"
65
+ current_worktree="$(pwd -P)"
66
+
67
+ if [[ -z "$SOURCE_BRANCH" ]]; then
68
+ SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
69
+ fi
70
+
71
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
72
+ echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
73
+ exit 1
74
+ fi
75
+
76
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
77
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
78
+ if [[ -n "$configured_base" ]]; then
79
+ BASE_BRANCH="$configured_base"
80
+ fi
81
+ fi
82
+
83
+ if [[ -z "$BASE_BRANCH" ]]; then
84
+ branch_stored_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.musafetyBase" || true)"
85
+ if [[ -n "$branch_stored_base" ]]; then
86
+ BASE_BRANCH="$branch_stored_base"
87
+ fi
88
+ fi
89
+
90
+ if [[ -z "$BASE_BRANCH" ]]; then
91
+ source_upstream="$(git -C "$repo_root" for-each-ref --format='%(upstream:short)' "refs/heads/${SOURCE_BRANCH}" | head -n 1)"
92
+ source_upstream="${source_upstream:-}"
93
+ if [[ "$source_upstream" == */* ]]; then
94
+ BASE_BRANCH="${source_upstream#*/}"
95
+ fi
96
+ fi
97
+
98
+ if [[ -z "$BASE_BRANCH" ]]; then
99
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
100
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" && "$current_branch" != "$SOURCE_BRANCH" ]]; then
101
+ BASE_BRANCH="$current_branch"
102
+ fi
103
+ fi
104
+
105
+ if [[ -z "$BASE_BRANCH" ]]; then
106
+ BASE_BRANCH="dev"
107
+ fi
108
+
109
+ if [[ "$SOURCE_BRANCH" == "$BASE_BRANCH" ]]; then
110
+ echo "[agent-branch-finish] Source branch and base branch are both '$BASE_BRANCH'." >&2
111
+ echo "[agent-branch-finish] Switch to your agent branch or pass --branch <agent-branch>." >&2
112
+ exit 1
113
+ fi
114
+
115
+ if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${SOURCE_BRANCH}"; then
116
+ echo "[agent-branch-finish] Local source branch does not exist: ${SOURCE_BRANCH}" >&2
117
+ exit 1
118
+ fi
119
+
120
+ get_worktree_for_branch() {
121
+ local branch="$1"
122
+ git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
123
+ $1 == "worktree" { wt = $2 }
124
+ $1 == "branch" && $2 == target { print wt; exit }
125
+ '
126
+ }
127
+
128
+ is_clean_worktree() {
129
+ local wt="$1"
130
+ git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
131
+ && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
132
+ }
133
+
134
+ source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
135
+ created_source_probe=0
136
+ source_probe_path=""
137
+
138
+ if [[ -z "$source_worktree" ]]; then
139
+ source_probe_path="${repo_root}/.omx/agent-worktrees/__source-probe-${SOURCE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
140
+ mkdir -p "$(dirname "$source_probe_path")"
141
+ git -C "$repo_root" worktree add "$source_probe_path" "$SOURCE_BRANCH" >/dev/null
142
+ source_worktree="$source_probe_path"
143
+ created_source_probe=1
144
+ fi
145
+
146
+ if ! is_clean_worktree "$source_worktree"; then
147
+ echo "[agent-branch-finish] Source worktree is not clean for '${SOURCE_BRANCH}': ${source_worktree}" >&2
148
+ echo "[agent-branch-finish] Commit/stash changes on the source branch before finishing." >&2
149
+ exit 1
150
+ fi
151
+
152
+ start_ref="$BASE_BRANCH"
153
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
154
+ git -C "$repo_root" fetch origin "$BASE_BRANCH" --quiet
155
+ start_ref="origin/${BASE_BRANCH}"
156
+ fi
157
+
158
+ require_before_finish_raw="$(git -C "$repo_root" config --get multiagent.sync.requireBeforeFinish || true)"
159
+ if [[ -z "$require_before_finish_raw" ]]; then
160
+ require_before_finish_raw="true"
161
+ fi
162
+ require_before_finish="$(printf '%s' "$require_before_finish_raw" | tr '[:upper:]' '[:lower:]')"
163
+ should_require_sync=0
164
+ case "$require_before_finish" in
165
+ 1|true|yes|on) should_require_sync=1 ;;
166
+ 0|false|no|off) should_require_sync=0 ;;
167
+ *) should_require_sync=1 ;;
168
+ esac
169
+
170
+ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
171
+ behind_count="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
172
+ behind_count="${behind_count:-0}"
173
+ if [[ "$behind_count" -gt 0 ]]; then
174
+ echo "[agent-sync-guard] Branch '${SOURCE_BRANCH}' is behind origin/${BASE_BRANCH} by ${behind_count} commit(s)." >&2
175
+ echo "[agent-sync-guard] Auto-syncing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH} before finish..." >&2
176
+ if ! git -C "$source_worktree" rebase "origin/${BASE_BRANCH}"; then
177
+ git_dir="$(git -C "$source_worktree" rev-parse --git-dir)"
178
+ rebase_active=0
179
+ if [[ -e "${git_dir}/rebase-merge" || -e "${git_dir}/rebase-apply" ]]; then
180
+ rebase_active=1
181
+ fi
182
+
183
+ echo "[agent-sync-guard] Auto-sync failed while rebasing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH}." >&2
184
+ if [[ "$rebase_active" -eq 1 ]]; then
185
+ echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
186
+ echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
187
+ fi
188
+ exit 1
189
+ fi
190
+
191
+ behind_after="$(git -C "$repo_root" rev-list --left-right --count "${SOURCE_BRANCH}...origin/${BASE_BRANCH}" 2>/dev/null | awk '{print $2}')"
192
+ behind_after="${behind_after:-0}"
193
+ echo "[agent-sync-guard] Auto-sync complete (behind now: ${behind_after})." >&2
194
+ fi
195
+ fi
196
+
197
+ integration_worktree="${repo_root}/.omx/agent-worktrees/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
198
+ integration_branch="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
199
+ mkdir -p "$(dirname "$integration_worktree")"
200
+
201
+ git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
202
+ git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null
203
+
204
+ cleanup() {
205
+ if [[ -d "$integration_worktree" ]]; then
206
+ git -C "$repo_root" worktree remove "$integration_worktree" --force >/dev/null 2>&1 || true
207
+ fi
208
+ if [[ "$created_source_probe" -eq 1 && -n "$source_probe_path" && -d "$source_probe_path" ]]; then
209
+ git -C "$repo_root" worktree remove "$source_probe_path" --force >/dev/null 2>&1 || true
210
+ fi
211
+ }
212
+ trap cleanup EXIT
213
+
214
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
215
+ git -C "$source_worktree" fetch origin "$BASE_BRANCH" --quiet
216
+
217
+ if ! git -C "$source_worktree" merge --no-commit --no-ff "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
218
+ conflict_files="$(git -C "$source_worktree" diff --name-only --diff-filter=U || true)"
219
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
220
+
221
+ echo "[agent-branch-finish] Preflight conflict detected between '${SOURCE_BRANCH}' and latest origin/${BASE_BRANCH}." >&2
222
+ if [[ -n "$conflict_files" ]]; then
223
+ echo "[agent-branch-finish] Conflicting files:" >&2
224
+ while IFS= read -r file; do
225
+ [[ -n "$file" ]] && echo " - ${file}" >&2
226
+ done <<< "$conflict_files"
227
+ fi
228
+ echo "[agent-branch-finish] Rebase/merge '${BASE_BRANCH}' into '${SOURCE_BRANCH}' and resolve conflicts before finishing." >&2
229
+ exit 1
230
+ fi
231
+
232
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
233
+ fi
234
+
235
+ if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
236
+ echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
237
+ git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
238
+ exit 1
239
+ fi
240
+
241
+ merge_completed=1
242
+ merge_status="direct"
243
+ direct_push_error=""
244
+ pr_url=""
245
+
246
+ is_local_branch_delete_error() {
247
+ local output="$1"
248
+ if [[ "$output" != *"failed to delete local branch"* ]]; then
249
+ return 1
250
+ fi
251
+ if [[ "$output" == *"cannot delete branch"* ]] || [[ "$output" == *"used by worktree"* ]]; then
252
+ return 0
253
+ fi
254
+ return 1
255
+ }
256
+
257
+ run_pr_flow() {
258
+ if ! command -v "$GH_BIN" >/dev/null 2>&1; then
259
+ echo "[agent-branch-finish] PR fallback requested but GitHub CLI not found: ${GH_BIN}" >&2
260
+ return 1
261
+ fi
262
+
263
+ git -C "$source_worktree" push -u origin "$SOURCE_BRANCH"
264
+
265
+ pr_title="$(git -C "$repo_root" log -1 --pretty=%s "$SOURCE_BRANCH" 2>/dev/null || true)"
266
+ if [[ -z "$pr_title" ]]; then
267
+ pr_title="Merge ${SOURCE_BRANCH} into ${BASE_BRANCH}"
268
+ fi
269
+ pr_body="Automated by scripts/agent-branch-finish.sh (PR flow)."
270
+
271
+ "$GH_BIN" pr create \
272
+ --base "$BASE_BRANCH" \
273
+ --head "$SOURCE_BRANCH" \
274
+ --title "$pr_title" \
275
+ --body "$pr_body" >/dev/null 2>&1 || true
276
+
277
+ pr_url="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json url --jq '.url' 2>/dev/null || true)"
278
+
279
+ merge_output=""
280
+ if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
281
+ return 0
282
+ fi
283
+ if is_local_branch_delete_error "$merge_output"; then
284
+ echo "[agent-branch-finish] PR merged but gh could not delete the local branch (active worktree); continuing local cleanup." >&2
285
+ return 0
286
+ fi
287
+
288
+ auto_output=""
289
+ if auto_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch --auto 2>&1)"; then
290
+ echo "[agent-branch-finish] PR auto-merge enabled; waiting for required checks/reviews." >&2
291
+ return 2
292
+ fi
293
+
294
+ if [[ -n "$merge_output" ]]; then
295
+ echo "[agent-branch-finish] PR merge not completed yet; leaving PR open." >&2
296
+ echo "${merge_output}" >&2
297
+ fi
298
+ if [[ -n "$auto_output" ]]; then
299
+ echo "${auto_output}" >&2
300
+ fi
301
+ return 2
302
+ }
303
+
304
+ if [[ "$PUSH_ENABLED" -eq 1 ]]; then
305
+ if [[ "$MERGE_MODE" != "pr" ]]; then
306
+ if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
307
+ direct_push_error="$direct_push_output"
308
+ merge_completed=0
309
+ fi
310
+ else
311
+ merge_completed=0
312
+ fi
313
+
314
+ if [[ "$merge_completed" -eq 0 ]]; then
315
+ if [[ "$MERGE_MODE" == "direct" ]]; then
316
+ echo "[agent-branch-finish] Direct push/merge failed in --direct-only mode." >&2
317
+ if [[ -n "$direct_push_error" ]]; then
318
+ echo "$direct_push_error" >&2
319
+ fi
320
+ exit 1
321
+ fi
322
+
323
+ if run_pr_flow; then
324
+ merge_completed=1
325
+ merge_status="pr"
326
+ else
327
+ pr_exit=$?
328
+ if [[ "$pr_exit" -eq 2 ]]; then
329
+ echo "[agent-branch-finish] PR flow created/updated branch '${SOURCE_BRANCH}' against '${BASE_BRANCH}'." >&2
330
+ if [[ -n "$pr_url" ]]; then
331
+ echo "[agent-branch-finish] PR: ${pr_url}" >&2
332
+ fi
333
+ echo "[agent-branch-finish] Merge pending review/check policy. Branch cleanup skipped for now." >&2
334
+ exit 0
335
+ fi
336
+ echo "[agent-branch-finish] PR flow failed." >&2
337
+ if [[ -n "$direct_push_error" ]]; then
338
+ echo "[agent-branch-finish] Direct push failure details:" >&2
339
+ echo "$direct_push_error" >&2
340
+ fi
341
+ exit 1
342
+ fi
343
+ fi
344
+ fi
345
+
346
+ if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
347
+ python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
348
+ fi
349
+
350
+ if [[ "$source_worktree" == "$repo_root" ]]; then
351
+ if is_clean_worktree "$source_worktree"; then
352
+ git -C "$source_worktree" checkout "$BASE_BRANCH" >/dev/null 2>&1 || true
353
+ if [[ "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
354
+ git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
355
+ fi
356
+ fi
357
+ elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
358
+ git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
359
+ fi
360
+
361
+ if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
362
+ git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
363
+ fi
364
+
365
+ git -C "$repo_root" branch -d "$SOURCE_BRANCH"
366
+
367
+ if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
368
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
369
+ git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
370
+ fi
371
+ fi
372
+
373
+ base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
374
+ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
375
+ git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
376
+ fi
377
+
378
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
379
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
380
+ echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
381
+ echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2
382
+ fi
383
+ fi
384
+
385
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and removed branch."
386
+ if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
387
+ echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
388
+ echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2
389
+ fi
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ TASK_NAME="task"
5
+ AGENT_NAME="agent"
6
+ BASE_BRANCH=""
7
+ BASE_BRANCH_EXPLICIT=0
8
+ WORKTREE_MODE=1
9
+ ALLOW_IN_PLACE=0
10
+ WORKTREE_ROOT_REL=".omx/agent-worktrees"
11
+ POSITIONAL_ARGS=()
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --task)
16
+ TASK_NAME="${2:-task}"
17
+ shift 2
18
+ ;;
19
+ --agent)
20
+ AGENT_NAME="${2:-agent}"
21
+ shift 2
22
+ ;;
23
+ --base)
24
+ BASE_BRANCH="${2:-}"
25
+ BASE_BRANCH_EXPLICIT=1
26
+ shift 2
27
+ ;;
28
+ --in-place)
29
+ WORKTREE_MODE=0
30
+ shift
31
+ ;;
32
+ --allow-in-place)
33
+ ALLOW_IN_PLACE=1
34
+ shift
35
+ ;;
36
+ --worktree-root)
37
+ WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
38
+ shift 2
39
+ ;;
40
+ --)
41
+ shift
42
+ while [[ $# -gt 0 ]]; do
43
+ POSITIONAL_ARGS+=("$1")
44
+ shift
45
+ done
46
+ break
47
+ ;;
48
+ -*)
49
+ echo "[agent-branch-start] Unknown option: $1" >&2
50
+ echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
51
+ exit 1
52
+ ;;
53
+ *)
54
+ POSITIONAL_ARGS+=("$1")
55
+ shift
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
61
+ echo "[agent-branch-start] Too many positional arguments." >&2
62
+ echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
63
+ exit 1
64
+ fi
65
+
66
+ if [[ "${#POSITIONAL_ARGS[@]}" -ge 1 ]]; then
67
+ TASK_NAME="${POSITIONAL_ARGS[0]}"
68
+ fi
69
+
70
+ if [[ "${#POSITIONAL_ARGS[@]}" -ge 2 ]]; then
71
+ AGENT_NAME="${POSITIONAL_ARGS[1]}"
72
+ fi
73
+
74
+ if [[ "${#POSITIONAL_ARGS[@]}" -ge 3 ]]; then
75
+ BASE_BRANCH="${POSITIONAL_ARGS[2]}"
76
+ BASE_BRANCH_EXPLICIT=1
77
+ fi
78
+
79
+ sanitize_slug() {
80
+ local raw="$1"
81
+ local fallback="${2:-task}"
82
+ local slug
83
+ slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
84
+ if [[ -z "$slug" ]]; then
85
+ slug="$fallback"
86
+ fi
87
+ printf '%s' "$slug"
88
+ }
89
+
90
+ resolve_active_codex_snapshot_name() {
91
+ local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
92
+ if [[ -n "$override" ]]; then
93
+ printf '%s' "$override"
94
+ return 0
95
+ fi
96
+
97
+ local codex_auth_bin="${MUSAFETY_CODEX_AUTH_BIN:-codex-auth}"
98
+ if ! command -v "$codex_auth_bin" >/dev/null 2>&1; then
99
+ return 0
100
+ fi
101
+
102
+ "$codex_auth_bin" list 2>/dev/null \
103
+ | sed -n 's/^[[:space:]]*\*[[:space:]]\+//p' \
104
+ | head -n 1 \
105
+ | tr -d '\r' || true
106
+ }
107
+
108
+ has_local_changes() {
109
+ local root="$1"
110
+ if ! git -C "$root" diff --quiet; then
111
+ return 0
112
+ fi
113
+ if ! git -C "$root" diff --cached --quiet; then
114
+ return 0
115
+ fi
116
+ if [[ -n "$(git -C "$root" ls-files --others --exclude-standard)" ]]; then
117
+ return 0
118
+ fi
119
+ return 1
120
+ }
121
+
122
+ has_tracked_changes() {
123
+ local root="$1"
124
+ if ! git -C "$root" diff --quiet; then
125
+ return 0
126
+ fi
127
+ if ! git -C "$root" diff --cached --quiet; then
128
+ return 0
129
+ fi
130
+ return 1
131
+ }
132
+
133
+ resolve_protected_branches() {
134
+ local root="$1"
135
+ local raw
136
+ raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git -C "$root" config --get multiagent.protectedBranches || true)}"
137
+ if [[ -z "$raw" ]]; then
138
+ raw="dev main master"
139
+ fi
140
+ raw="${raw//,/ }"
141
+ printf '%s' "$raw"
142
+ }
143
+
144
+ is_protected_branch_name() {
145
+ local branch="$1"
146
+ local protected_raw="$2"
147
+ for protected_branch in $protected_raw; do
148
+ if [[ "$branch" == "$protected_branch" ]]; then
149
+ return 0
150
+ fi
151
+ done
152
+ return 1
153
+ }
154
+
155
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
156
+ echo "[agent-branch-start] Not inside a git repository." >&2
157
+ exit 1
158
+ fi
159
+
160
+ repo_root="$(git rev-parse --show-toplevel)"
161
+
162
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
163
+ echo "[agent-branch-start] --base requires a non-empty branch name." >&2
164
+ exit 1
165
+ fi
166
+
167
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
168
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
169
+ if [[ -n "$configured_base" ]]; then
170
+ BASE_BRANCH="$configured_base"
171
+ else
172
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
173
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
174
+ BASE_BRANCH="$current_branch"
175
+ else
176
+ BASE_BRANCH="dev"
177
+ fi
178
+ fi
179
+ fi
180
+
181
+ if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
182
+ git fetch origin "${BASE_BRANCH}" --quiet
183
+ start_ref="origin/${BASE_BRANCH}"
184
+ else
185
+ if ! git show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
186
+ echo "[agent-branch-start] Base branch not found locally or on origin: ${BASE_BRANCH}" >&2
187
+ exit 1
188
+ fi
189
+ start_ref="${BASE_BRANCH}"
190
+ fi
191
+
192
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
193
+ agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
194
+ snapshot_name="$(resolve_active_codex_snapshot_name)"
195
+ snapshot_slug="$(sanitize_slug "$snapshot_name" "")"
196
+ timestamp="$(date +%Y%m%d-%H%M%S)"
197
+ if [[ -n "$snapshot_slug" ]]; then
198
+ branch_name="agent/${agent_slug}/${timestamp}-${snapshot_slug}-${task_slug}"
199
+ else
200
+ branch_name="agent/${agent_slug}/${timestamp}-${task_slug}"
201
+ fi
202
+
203
+ if git show-ref --verify --quiet "refs/heads/${branch_name}"; then
204
+ echo "[agent-branch-start] Branch already exists: ${branch_name}" >&2
205
+ exit 1
206
+ fi
207
+
208
+ if [[ "$WORKTREE_MODE" -eq 0 ]]; then
209
+ if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
210
+ echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
211
+ echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
212
+ exit 1
213
+ fi
214
+
215
+ if ! git diff --quiet || ! git diff --cached --quiet; then
216
+ echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
217
+ exit 1
218
+ fi
219
+
220
+ current_branch="$(git rev-parse --abbrev-ref HEAD)"
221
+ if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
222
+ git checkout "$BASE_BRANCH"
223
+ fi
224
+
225
+ if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
226
+ git pull --ff-only origin "$BASE_BRANCH"
227
+ fi
228
+
229
+ git checkout -b "$branch_name"
230
+ git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
231
+ echo "[agent-branch-start] Created in-place branch: ${branch_name}"
232
+ echo "$branch_name"
233
+ exit 0
234
+ fi
235
+
236
+ worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
237
+ mkdir -p "$worktree_root"
238
+ worktree_path="${worktree_root}/${branch_name//\//__}"
239
+
240
+ if [[ -e "$worktree_path" ]]; then
241
+ echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
242
+ exit 1
243
+ fi
244
+
245
+ auto_transfer_stash_ref=""
246
+ auto_transfer_message=""
247
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
248
+ protected_branches_raw="$(resolve_protected_branches "$repo_root")"
249
+ if [[ "$current_branch" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_BRANCH" "$protected_branches_raw"; then
250
+ if has_tracked_changes "$repo_root"; then
251
+ auto_transfer_message="musafety-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
252
+ if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
253
+ auto_transfer_stash_ref="$(
254
+ git -C "$repo_root" stash list \
255
+ | awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
256
+ )"
257
+ if [[ -n "$auto_transfer_stash_ref" ]]; then
258
+ echo "[agent-branch-start] Detected local changes on protected base '${BASE_BRANCH}'. Moving them to '${branch_name}'..."
259
+ fi
260
+ fi
261
+ fi
262
+ fi
263
+
264
+ git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref"
265
+ git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
266
+
267
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
268
+ git -C "$worktree_path" branch --set-upstream-to="origin/${BASE_BRANCH}" "$branch_name" >/dev/null 2>&1 || true
269
+ fi
270
+
271
+ if [[ -n "$auto_transfer_stash_ref" ]]; then
272
+ if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
273
+ git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
274
+ echo "[agent-branch-start] Moved local changes from '${BASE_BRANCH}' into '${branch_name}'."
275
+ else
276
+ echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
277
+ echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${BASE_BRANCH}." >&2
278
+ echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
279
+ exit 1
280
+ fi
281
+ fi
282
+
283
+ echo "[agent-branch-start] Created branch: ${branch_name}"
284
+ echo "[agent-branch-start] Worktree: ${worktree_path}"
285
+ echo "[agent-branch-start] Next steps:"
286
+ echo " cd \"${worktree_path}\""
287
+ echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
288
+ echo " # implement + commit"
289
+ echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\""