From ea7f6123673f35b02a527b5f066ac9738be1526b Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Mon, 13 Apr 2026 20:04:16 +0200 Subject: [PATCH] Keep sandbox automation anchored to configured/dev base by default Operators wanted new agent sandboxes and review-bot monitoring to stay on the configured base (or dev) instead of drifting to whichever branch is currently checked out. This updates runtime + template scripts and adds coverage for base-selection behavior. Constraint: Preserve explicit --base overrides and existing branch metadata handling Rejected: Always hardcode main | repos in this toolchain default to dev and may configure multiagent.baseBranch Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep runtime scripts and templates in lockstep for branch-base resolution Tested: bash -n scripts/agent-branch-start.sh scripts/review-bot-watch.sh templates/scripts/agent-branch-start.sh templates/scripts/review-bot-watch.sh Tested: node --test --test-name-pattern 'review-bot-watch script prints help after setup' test/install.test.js Tested: node --test --test-name-pattern 'setup agent-branch-start defaults base to configured/dev and keeps current checkout branch unchanged' test/install.test.js Not-tested: node --test --test-name-pattern 'codex-agent defaults to configured/dev base and keeps current checkout branch unchanged' test/install.test.js (timed out) --- scripts/agent-branch-start.sh | 38 +++++++---- scripts/review-bot-watch.sh | 12 +++- templates/scripts/agent-branch-start.sh | 38 +++++++---- templates/scripts/review-bot-watch.sh | 12 +++- test/install.test.js | 84 ++++++++++++++++++++++--- 5 files changed, 150 insertions(+), 34 deletions(-) diff --git a/scripts/agent-branch-start.sh b/scripts/agent-branch-start.sh index 40367b2..ff4e74d 100755 --- a/scripts/agent-branch-start.sh +++ b/scripts/agent-branch-start.sh @@ -141,6 +141,32 @@ resolve_protected_branches() { printf '%s' "$raw" } +resolve_default_base_branch() { + local root="$1" + local configured_base preferred_base current_branch + + configured_base="$(git -C "$root" config --get multiagent.baseBranch || true)" + if [[ -n "$configured_base" ]]; then + printf '%s' "$configured_base" + return 0 + fi + + preferred_base="${MUSAFETY_BASE_BRANCH_DEFAULT:-dev}" + if git -C "$root" show-ref --verify --quiet "refs/remotes/origin/${preferred_base}" \ + || git -C "$root" show-ref --verify --quiet "refs/heads/${preferred_base}"; then + printf '%s' "$preferred_base" + return 0 + fi + + current_branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then + printf '%s' "$current_branch" + return 0 + fi + + printf '%s' "$preferred_base" +} + is_protected_branch_name() { local branch="$1" local protected_raw="$2" @@ -195,17 +221,7 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then fi if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then - configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)" - if [[ -n "$configured_base" ]]; then - BASE_BRANCH="$configured_base" - else - current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" - if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then - BASE_BRANCH="$current_branch" - else - BASE_BRANCH="dev" - fi - fi + BASE_BRANCH="$(resolve_default_base_branch "$repo_root")" fi if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then diff --git a/scripts/review-bot-watch.sh b/scripts/review-bot-watch.sh index dd43e5d..a2a1bc9 100755 --- a/scripts/review-bot-watch.sh +++ b/scripts/review-bot-watch.sh @@ -18,7 +18,7 @@ Continuously monitor GitHub pull requests targeting a base branch and dispatch one Codex-agent task per newly opened/updated PR. Options: - --base Base branch to watch (default: current branch) + --base Base branch to watch (default: multiagent.baseBranch or dev) --interval Poll interval (default: 30) --agent Agent name for codex-agent (default: guardex-review-bot) --task-prefix Task prefix for codex-agent branches (default: review-merge) @@ -117,6 +117,16 @@ fi repo_root="$(git rev-parse --show-toplevel)" if [[ -z "$BASE_BRANCH" ]]; then + BASE_BRANCH="$(git -C "$repo_root" config --get multiagent.baseBranch 2>/dev/null || true)" +fi +if [[ -z "$BASE_BRANCH" ]]; then + preferred_base="${MUSAFETY_BASE_BRANCH_DEFAULT:-dev}" + if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${preferred_base}" \ + || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${preferred_base}"; then + BASE_BRANCH="$preferred_base" + fi +fi +if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then BASE_BRANCH="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" fi if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then diff --git a/templates/scripts/agent-branch-start.sh b/templates/scripts/agent-branch-start.sh index 40367b2..ff4e74d 100755 --- a/templates/scripts/agent-branch-start.sh +++ b/templates/scripts/agent-branch-start.sh @@ -141,6 +141,32 @@ resolve_protected_branches() { printf '%s' "$raw" } +resolve_default_base_branch() { + local root="$1" + local configured_base preferred_base current_branch + + configured_base="$(git -C "$root" config --get multiagent.baseBranch || true)" + if [[ -n "$configured_base" ]]; then + printf '%s' "$configured_base" + return 0 + fi + + preferred_base="${MUSAFETY_BASE_BRANCH_DEFAULT:-dev}" + if git -C "$root" show-ref --verify --quiet "refs/remotes/origin/${preferred_base}" \ + || git -C "$root" show-ref --verify --quiet "refs/heads/${preferred_base}"; then + printf '%s' "$preferred_base" + return 0 + fi + + current_branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then + printf '%s' "$current_branch" + return 0 + fi + + printf '%s' "$preferred_base" +} + is_protected_branch_name() { local branch="$1" local protected_raw="$2" @@ -195,17 +221,7 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then fi if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then - configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)" - if [[ -n "$configured_base" ]]; then - BASE_BRANCH="$configured_base" - else - current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" - if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then - BASE_BRANCH="$current_branch" - else - BASE_BRANCH="dev" - fi - fi + BASE_BRANCH="$(resolve_default_base_branch "$repo_root")" fi if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then diff --git a/templates/scripts/review-bot-watch.sh b/templates/scripts/review-bot-watch.sh index dd43e5d..a2a1bc9 100755 --- a/templates/scripts/review-bot-watch.sh +++ b/templates/scripts/review-bot-watch.sh @@ -18,7 +18,7 @@ Continuously monitor GitHub pull requests targeting a base branch and dispatch one Codex-agent task per newly opened/updated PR. Options: - --base Base branch to watch (default: current branch) + --base Base branch to watch (default: multiagent.baseBranch or dev) --interval Poll interval (default: 30) --agent Agent name for codex-agent (default: guardex-review-bot) --task-prefix Task prefix for codex-agent branches (default: review-merge) @@ -117,6 +117,16 @@ fi repo_root="$(git rev-parse --show-toplevel)" if [[ -z "$BASE_BRANCH" ]]; then + BASE_BRANCH="$(git -C "$repo_root" config --get multiagent.baseBranch 2>/dev/null || true)" +fi +if [[ -z "$BASE_BRANCH" ]]; then + preferred_base="${MUSAFETY_BASE_BRANCH_DEFAULT:-dev}" + if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${preferred_base}" \ + || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${preferred_base}"; then + BASE_BRANCH="$preferred_base" + fi +fi +if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then BASE_BRANCH="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" fi if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then diff --git a/test/install.test.js b/test/install.test.js index a146a3d..31ba875 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -291,6 +291,7 @@ test('review-bot-watch script prints help after setup', () => { const helpResult = runCmd('bash', ['scripts/review-bot-watch.sh', '--help'], repoDir); assert.equal(helpResult.status, 0, helpResult.stderr || helpResult.stdout); assert.match(helpResult.stdout, /Continuously monitor GitHub pull requests targeting a base branch/); + assert.match(helpResult.stdout, /default: multiagent\.baseBranch or dev/); }); test('review-bot-watch uses explicit codex-agent flags for argument parsing compatibility', () => { @@ -635,12 +636,14 @@ test('setup agent-branch-start supports explicit snapshot override without codex assert.match(result.stdout, /Created branch: agent\/bot\/prod-snapshot-one-ship-fix(?:-\d+)?/); }); -test('setup agent-branch-start defaults base to current branch and stores per-branch base metadata', () => { - const repoDir = initRepoOnBranch('main'); +test('setup agent-branch-start defaults base to configured\/dev and keeps current checkout unchanged', () => { + const repoDir = initRepo(); seedCommit(repoDir); - attachOriginRemoteForBranch(repoDir, 'main'); + attachOriginRemote(repoDir); + let result = runCmd('git', ['checkout', '-b', 'main'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); - let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); assert.equal(result.status, 0, result.stderr || result.stdout); result = runCmd('git', ['add', '.'], repoDir); @@ -649,21 +652,26 @@ test('setup agent-branch-start defaults base to current branch and stores per-br ALLOW_COMMIT_ON_PROTECTED_BRANCH: '1', }); assert.equal(result.status, 0, result.stderr || result.stdout); - result = runCmd('git', ['push', 'origin', 'main'], repoDir); - assert.equal(result.status, 0, result.stderr || result.stdout); - result = runCmd('bash', ['scripts/agent-branch-start.sh', 'auto-base', 'bot'], repoDir); assert.equal(result.status, 0, result.stderr || result.stdout); const agentBranch = extractCreatedBranch(result.stdout); const agentWorktree = extractCreatedWorktree(result.stdout); - const upstream = runCmd('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], agentWorktree); + const upstream = runCmd( + 'git', + ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], + agentWorktree, + ); assert.equal(upstream.status, 0, upstream.stderr || upstream.stdout); - assert.equal(upstream.stdout.trim(), 'origin/main'); + assert.equal(upstream.stdout.trim(), 'origin/dev'); const storedBase = runCmd('git', ['config', '--get', `branch.${agentBranch}.musafetyBase`], repoDir); assert.equal(storedBase.status, 0, storedBase.stderr || storedBase.stdout); - assert.equal(storedBase.stdout.trim(), 'main'); + assert.equal(storedBase.stdout.trim(), 'dev'); + + const currentBranch = runCmd('git', ['branch', '--show-current'], repoDir); + assert.equal(currentBranch.status, 0, currentBranch.stderr || currentBranch.stdout); + assert.equal(currentBranch.stdout.trim(), 'main'); }); test('agent-branch-start moves protected-branch local changes into the new agent worktree', () => { @@ -1104,6 +1112,62 @@ test('codex-agent launches codex inside a fresh sandbox worktree and keeps branc assert.equal(branchResult.status, 0, 'agent branch should remain after default codex-agent run'); }); +test('codex-agent defaults to configured/dev base and keeps current checkout branch unchanged', () => { + const repoDir = initRepo(); + seedCommit(repoDir); + attachOriginRemote(repoDir); + let result = runCmd('git', ['checkout', '-b', 'main'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd('git', ['add', '.'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd('git', ['commit', '-m', 'apply gx setup'], repoDir, { + ALLOW_COMMIT_ON_PROTECTED_BRANCH: '1', + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'musafety-fake-codex-default-base-')); + const fakeCodexPath = path.join(fakeBin, 'codex'); + fs.writeFileSync( + fakeCodexPath, + `#!/usr/bin/env bash\n` + + `pwd > "${'${MUSAFETY_TEST_CODEX_CWD}'}"\n` + + `echo "$@" > "${'${MUSAFETY_TEST_CODEX_ARGS}'}"\n`, + 'utf8', + ); + fs.chmodSync(fakeCodexPath, 0o755); + + const cwdMarker = path.join(repoDir, '.codex-agent-default-base-cwd'); + const argsMarker = path.join(repoDir, '.codex-agent-default-base-args'); + const launch = runCmd( + 'bash', + ['scripts/codex-agent.sh', 'launch-task', 'planner', '--model', 'gpt-5.4-mini'], + repoDir, + { + PATH: `${fakeBin}:${process.env.PATH}`, + MUSAFETY_TEST_CODEX_CWD: cwdMarker, + MUSAFETY_TEST_CODEX_ARGS: argsMarker, + }, + ); + assert.equal(launch.status, 0, launch.stderr || launch.stdout); + const launchedBranch = extractCreatedBranch(launch.stdout); + const launchedCwd = fs.readFileSync(cwdMarker, 'utf8').trim(); + + const upstream = runCmd('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], launchedCwd); + assert.equal(upstream.status, 0, upstream.stderr || upstream.stdout); + assert.equal(upstream.stdout.trim(), 'origin/dev'); + + const storedBase = runCmd('git', ['config', '--get', `branch.${launchedBranch}.musafetyBase`], repoDir); + assert.equal(storedBase.status, 0, storedBase.stderr || storedBase.stdout); + assert.equal(storedBase.stdout.trim(), 'dev'); + + const currentBranch = runCmd('git', ['branch', '--show-current'], repoDir); + assert.equal(currentBranch.status, 0, currentBranch.stderr || currentBranch.stdout); + assert.equal(currentBranch.stdout.trim(), 'main'); +}); + test('codex-agent supports --codex-bin override before positional arguments', () => { const repoDir = initRepo(); seedCommit(repoDir);