Skip to content

Commit 16d3d45

Browse files
NagyViktNagyVikt
andauthored
Unblock protected-base branch finish flows without weakening main safeguards (#10)
This rebases the old protected-branch PR on top of current main and keeps only the still-useful delta: template pre-push guardrails plus branch-finish PR fallback modes. The AGENTS template line is updated so generated guidance matches the actual finish behavior. Constraint: Keep current main hook policy intact while making legacy PR branch mergeable Rejected: Merge old branch content wholesale | reintroduced stale protections and broke current tests Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep AGENTS template text synchronized with actual script behavior when finish-flow options change Tested: npm test (41/41 pass) Not-tested: live GitHub protected-base merge under required-review policy Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 14f505e commit 16d3d45

3 files changed

Lines changed: 169 additions & 4 deletions

File tree

templates/AGENTS.multiagent-safety.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
1111
- If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
1212
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
13-
- Agent completion must use `scripts/agent-branch-finish.sh` (merge into `dev`, push, delete agent branch).
13+
- Agent completion must use `scripts/agent-branch-finish.sh` (direct merge to base when allowed; auto PR fallback for protected bases, then cleanup after merge).
1414

1515
1. Explicit ownership before edits
1616

templates/githooks/pre-push

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ "${ALLOW_PUSH_ON_PROTECTED_BRANCH:-0}" == "1" || "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
5+
exit 0
6+
fi
7+
8+
is_vscode_git_context=0
9+
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
10+
is_vscode_git_context=1
11+
fi
12+
13+
if [[ "$is_vscode_git_context" == "1" ]]; then
14+
exit 0
15+
fi
16+
17+
protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
18+
if [[ -z "$protected_branches_raw" ]]; then
19+
protected_branches_raw="dev main master"
20+
fi
21+
protected_branches_raw="${protected_branches_raw//,/ }"
22+
23+
is_protected_branch() {
24+
local branch="$1"
25+
for protected_branch in $protected_branches_raw; do
26+
if [[ "$branch" == "$protected_branch" ]]; then
27+
return 0
28+
fi
29+
done
30+
return 1
31+
}
32+
33+
blocked_refs=()
34+
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
35+
if [[ -z "${remote_ref:-}" || "$remote_ref" != refs/heads/* ]]; then
36+
continue
37+
fi
38+
39+
remote_branch="${remote_ref#refs/heads/}"
40+
if is_protected_branch "$remote_branch"; then
41+
blocked_refs+=("$remote_branch")
42+
fi
43+
done
44+
45+
if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
46+
{
47+
echo "[agent-branch-guard] Push to protected branch blocked outside VS Code Git context."
48+
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
49+
echo "[agent-branch-guard] Use VS Code Source Control for protected-branch push, or push from an agent branch and merge via PR."
50+
echo
51+
echo "Temporary bypass (not recommended):"
52+
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
53+
} >&2
54+
exit 1
55+
fi
56+
57+
exit 0

templates/scripts/agent-branch-finish.sh

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ BASE_BRANCH_EXPLICIT=0
66
SOURCE_BRANCH=""
77
PUSH_ENABLED=1
88
DELETE_REMOTE_BRANCH=1
9+
MERGE_MODE="auto"
10+
GH_BIN="${MUSAFETY_GH_BIN:-gh}"
911

1012
while [[ $# -gt 0 ]]; do
1113
case "$1" in
@@ -26,14 +28,34 @@ while [[ $# -gt 0 ]]; do
2628
DELETE_REMOTE_BRANCH=0
2729
shift
2830
;;
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+
;;
2943
*)
3044
echo "[agent-branch-finish] Unknown argument: $1" >&2
31-
echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--keep-remote-branch]" >&2
45+
echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--keep-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
3246
exit 1
3347
;;
3448
esac
3549
done
3650

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+
3759
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
3860
echo "[agent-branch-finish] Not inside a git repository." >&2
3961
exit 1
@@ -169,8 +191,94 @@ if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; th
169191
exit 1
170192
fi
171193

194+
merge_completed=1
195+
merge_status="direct"
196+
direct_push_error=""
197+
pr_url=""
198+
199+
run_pr_flow() {
200+
if ! command -v "$GH_BIN" >/dev/null 2>&1; then
201+
echo "[agent-branch-finish] PR fallback requested but GitHub CLI not found: ${GH_BIN}" >&2
202+
return 1
203+
fi
204+
205+
git -C "$source_worktree" push -u origin "$SOURCE_BRANCH"
206+
207+
pr_title="$(git -C "$repo_root" log -1 --pretty=%s "$SOURCE_BRANCH" 2>/dev/null || true)"
208+
if [[ -z "$pr_title" ]]; then
209+
pr_title="Merge ${SOURCE_BRANCH} into ${BASE_BRANCH}"
210+
fi
211+
pr_body="Automated by scripts/agent-branch-finish.sh (PR flow)."
212+
213+
"$GH_BIN" pr create \
214+
--base "$BASE_BRANCH" \
215+
--head "$SOURCE_BRANCH" \
216+
--title "$pr_title" \
217+
--body "$pr_body" >/dev/null 2>&1 || true
218+
219+
pr_url="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json url --jq '.url' 2>/dev/null || true)"
220+
221+
merge_output=""
222+
if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
223+
return 0
224+
fi
225+
226+
auto_output=""
227+
if auto_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch --auto 2>&1)"; then
228+
echo "[agent-branch-finish] PR auto-merge enabled; waiting for required checks/reviews." >&2
229+
return 2
230+
fi
231+
232+
if [[ -n "$merge_output" ]]; then
233+
echo "[agent-branch-finish] PR merge not completed yet; leaving PR open." >&2
234+
echo "${merge_output}" >&2
235+
fi
236+
if [[ -n "$auto_output" ]]; then
237+
echo "${auto_output}" >&2
238+
fi
239+
return 2
240+
}
241+
172242
if [[ "$PUSH_ENABLED" -eq 1 ]]; then
173-
git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}"
243+
if [[ "$MERGE_MODE" != "pr" ]]; then
244+
if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
245+
direct_push_error="$direct_push_output"
246+
merge_completed=0
247+
fi
248+
else
249+
merge_completed=0
250+
fi
251+
252+
if [[ "$merge_completed" -eq 0 ]]; then
253+
if [[ "$MERGE_MODE" == "direct" ]]; then
254+
echo "[agent-branch-finish] Direct push/merge failed in --direct-only mode." >&2
255+
if [[ -n "$direct_push_error" ]]; then
256+
echo "$direct_push_error" >&2
257+
fi
258+
exit 1
259+
fi
260+
261+
if run_pr_flow; then
262+
merge_completed=1
263+
merge_status="pr"
264+
else
265+
pr_exit=$?
266+
if [[ "$pr_exit" -eq 2 ]]; then
267+
echo "[agent-branch-finish] PR flow created/updated branch '${SOURCE_BRANCH}' against '${BASE_BRANCH}'." >&2
268+
if [[ -n "$pr_url" ]]; then
269+
echo "[agent-branch-finish] PR: ${pr_url}" >&2
270+
fi
271+
echo "[agent-branch-finish] Merge pending review/check policy. Branch cleanup skipped for now." >&2
272+
exit 0
273+
fi
274+
echo "[agent-branch-finish] PR flow failed." >&2
275+
if [[ -n "$direct_push_error" ]]; then
276+
echo "[agent-branch-finish] Direct push failure details:" >&2
277+
echo "$direct_push_error" >&2
278+
fi
279+
exit 1
280+
fi
281+
fi
174282
fi
175283

176284
if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
@@ -209,7 +317,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
209317
fi
210318
fi
211319

212-
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' and removed branch."
320+
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and removed branch."
213321
if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
214322
echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
215323
echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2

0 commit comments

Comments
 (0)