Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-21
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Why

- `codex-agent` currently skips auto-finish whenever `origin` is a local-path remote, even when tests intentionally inject a working fake `gh` binary through `GUARDEX_GH_BIN`.
- Nested recursive-doctor tests create a fresh git repo under `frontend/` and call `seedCommit()` before any local identity exists, which now fails on CI runners without a global git identity.

## What Changes

- Allow the codex auto-finish path to use PR flow when the caller explicitly overrides the GitHub CLI binary via `GUARDEX_GH_BIN`, even if the repo remote is a local bare path.
- Ensure shared install-test helpers seed a local git identity before `seedCommit()` so nested repos can commit deterministically in CI.

## Impact

- Affects `scripts/codex-agent.sh`, `templates/scripts/codex-agent.sh`, and `test/install.test.js`.
- Keeps the existing skip behavior for local-path remotes when no explicit `GUARDEX_GH_BIN` override is provided.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## ADDED Requirements

### Requirement: codex-agent auto-finish respects explicit GitHub CLI overrides
`codex-agent` SHALL allow the PR-based auto-finish path to run when the caller explicitly sets `GUARDEX_GH_BIN`, even if the repo's `origin` URL is a local-path remote used by tests.

#### Scenario: Local-path origin with explicit GitHub CLI override
- **GIVEN** a repo whose `origin` remote is a local bare path
- **AND** `GUARDEX_GH_BIN` points to an executable CLI shim
- **WHEN** `codex-agent` runs with auto-finish enabled
- **THEN** it SHALL invoke the PR-based finish flow instead of skipping auto-finish because of the local-path remote.

### Requirement: shared install-test helpers seed local git identity
Shared install-test helpers SHALL configure a local git author identity before creating seed commits in ad hoc nested repos.

#### Scenario: Nested frontend repo seed commit
- **GIVEN** a nested git repo created directly inside an install test
- **WHEN** `seedCommit()` prepares the initial commit
- **THEN** the helper SHALL configure local `user.name` and `user.email` first
- **AND** the seed commit SHALL not depend on any global git identity on the runner.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28`.
- [x] 1.2 Define normative requirements in `specs/codex-agent-autofinish/spec.md`.

## 2. Implementation

- [x] 2.1 Implement scoped behavior changes.
- [x] 2.2 Add/update focused regression coverage.

## 3. Verification

- [x] 3.1 Run targeted project verification commands.
- [x] 3.2 Run `openspec validate agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Completion

- [ ] 4.1 Finish the agent branch via PR merge + cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `bash scripts/agent-branch-finish.sh --branch <agent-branch> --base <base-branch> --via-pr --wait-for-merge --cleanup`).
- [ ] 4.2 Record PR URL + final `MERGED` state in the completion handoff.
- [ ] 4.3 Confirm sandbox cleanup (`git worktree list`, `git branch -a`) or capture a `BLOCKED:` handoff if merge/cleanup is pending.
150 changes: 93 additions & 57 deletions scripts/codex-agent.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail

TASK_NAME="${MUSAFETY_TASK_NAME:-task}"
AGENT_NAME="${MUSAFETY_AGENT_NAME:-agent}"
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
TASK_NAME="${GUARDEX_TASK_NAME:-task}"
AGENT_NAME="${GUARDEX_AGENT_NAME:-agent}"
BASE_BRANCH="${GUARDEX_BASE_BRANCH:-}"
BASE_BRANCH_EXPLICIT=0
CODEX_BIN="${MUSAFETY_CODEX_BIN:-codex}"
AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}"
AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}"
OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}"
OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}"
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}"
CODEX_BIN="${GUARDEX_CODEX_BIN:-codex}"
AUTO_FINISH_RAW="${GUARDEX_CODEX_AUTO_FINISH:-true}"
AUTO_REVIEW_ON_CONFLICT_RAW="${GUARDEX_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
AUTO_CLEANUP_RAW="${GUARDEX_CODEX_AUTO_CLEANUP:-true}"
AUTO_WAIT_FOR_MERGE_RAW="${GUARDEX_CODEX_WAIT_FOR_MERGE:-true}"
OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"

normalize_bool() {
local raw="${1:-}"
Expand Down Expand Up @@ -130,6 +130,23 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
fi
repo_root="$(git rev-parse --show-toplevel)"

guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
if [[ -f "$guardex_env_helper" ]]; then
# shellcheck source=/dev/null
source "$guardex_env_helper"
fi
if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
toggle_source="$(guardex_repo_toggle_source "$repo_root" || true)"
toggle_raw="$(guardex_repo_toggle_raw "$repo_root" || true)"
if [[ -n "$toggle_source" && -n "$toggle_raw" ]]; then
echo "[codex-agent] Guardex is disabled for this repo (${toggle_source}: GUARDEX_ON=${toggle_raw})." >&2
else
echo "[codex-agent] Guardex is disabled for this repo." >&2
fi
echo "[codex-agent] Skip Guardex sandbox flow or re-enable with GUARDEX_ON=1." >&2
exit 1
fi

sanitize_slug() {
local raw="$1"
local fallback="${2:-task}"
Expand Down Expand Up @@ -202,32 +219,6 @@ hydrate_local_helper_in_worktree() {
echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}"
}

resolve_local_helper_script_path() {
local worktree="$1"
local relative_path="$2"
local candidate=""

candidate="${worktree}/${relative_path}"
if [[ -f "$candidate" ]]; then
printf '%s' "$candidate"
return 0
fi

candidate="${repo_root}/${relative_path}"
if [[ -f "$candidate" ]]; then
printf '%s' "$candidate"
return 0
fi

candidate="${repo_root}/templates/${relative_path}"
if [[ -f "$candidate" ]]; then
printf '%s' "$candidate"
return 0
fi

return 1
}

resolve_start_base_branch() {
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
printf '%s' "$BASE_BRANCH"
Expand Down Expand Up @@ -300,11 +291,13 @@ start_sandbox_fallback() {
return 1
fi

git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
local worktree_add_output=""
if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" 2>&1)"; then
printf '%s\n' "$worktree_add_output" >&2
return 1
fi
git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$base_branch" >/dev/null 2>&1 || true
git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true

printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
Expand All @@ -324,7 +317,7 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
start_output=""
start_status=0
set +e
start_output="$(MUSAFETY_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
start_output="$(GUARDEX_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
start_status=$?
set -e

Expand Down Expand Up @@ -379,6 +372,30 @@ has_origin_remote() {
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
}

has_explicit_gh_bin_override() {
[[ -n "${GUARDEX_GH_BIN:-}" ]]
}

gh_cli_available() {
local gh_bin="${GUARDEX_GH_BIN:-gh}"
if [[ "$gh_bin" == */* ]]; then
[[ -x "$gh_bin" ]]
return
fi
command -v "$gh_bin" >/dev/null 2>&1
}

origin_remote_supports_pr_finish() {
local origin_url
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
case "$origin_url" in
''|/*|./*|../*|file://*)
return 1
;;
esac
return 0
}

resolve_worktree_base_branch() {
local _wt="$1"
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
Expand Down Expand Up @@ -446,19 +463,24 @@ ensure_openspec_plan_workspace() {
return 0
fi

local openspec_script
if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-plan-workspace.sh")"; then
hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh"

local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh"
if [[ ! -f "$openspec_script" ]]; then
echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
return 1
fi
if [[ ! -x "$openspec_script" ]]; then
chmod +x "$openspec_script" 2>/dev/null || true
fi

local plan_slug
plan_slug="$(resolve_openspec_plan_slug "$branch")"
local init_output=""
if ! init_output="$(
cd "$wt"
bash "$openspec_script" "$plan_slug" 2>&1
bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
)"; then
printf '%s\n' "$init_output" >&2
echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
Expand All @@ -478,19 +500,24 @@ ensure_openspec_change_workspace() {
return 0
fi

local openspec_script
if ! openspec_script="$(resolve_local_helper_script_path "$wt" "scripts/openspec/init-change-workspace.sh")"; then
hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-change-workspace.sh"

local openspec_script="${wt}/scripts/openspec/init-change-workspace.sh"
if [[ ! -f "$openspec_script" ]]; then
echo "[codex-agent] Missing OpenSpec change init script in sandbox: ${openspec_script}" >&2
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
return 1
fi
if [[ ! -x "$openspec_script" ]]; then
chmod +x "$openspec_script" 2>/dev/null || true
fi

local change_slug capability_slug init_output=""
change_slug="$(resolve_openspec_change_slug "$branch")"
capability_slug="$(resolve_openspec_capability_slug)"
if ! init_output="$(
cd "$wt"
bash "$openspec_script" "$change_slug" "$capability_slug" 2>&1
bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
)"; then
printf '%s\n' "$init_output" >&2
echo "[codex-agent] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
Expand All @@ -501,6 +528,7 @@ ensure_openspec_change_workspace() {
fi
echo "[codex-agent] OpenSpec change workspace: ${wt}/openspec/changes/${change_slug}"
}

worktree_has_changes() {
local wt="$1"
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
Expand All @@ -520,7 +548,7 @@ claim_changed_files() {
local branch="$2"
local lock_script="${repo_root}/scripts/agent-file-locks.py"

if [[ ! -x "$lock_script" ]]; then
if [[ ! -f "$lock_script" ]]; then
return 0
fi

Expand Down Expand Up @@ -552,18 +580,18 @@ auto_commit_worktree_changes() {
local branch="$2"

if ! worktree_has_changes "$wt"; then
return 0
return 2
fi

claim_changed_files "$wt" "$branch"
git -C "$wt" add -A

if git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
return 0
return 2
fi

local default_message="Auto-finish: ${TASK_NAME}"
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
local commit_message="${GUARDEX_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
local commit_output=""

if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
Expand Down Expand Up @@ -677,11 +705,16 @@ run_finish_flow() {
fi

if has_origin_remote; then
if ! command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1 && ! command -v gh >/dev/null 2>&1; then
echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${MUSAFETY_GH_BIN:-gh}" >&2
if ! gh_cli_available; then
echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${GUARDEX_GH_BIN:-gh}" >&2
return 2
fi
if [[ -n "${GUARDEX_GH_BIN:-}" ]] || origin_remote_supports_pr_finish; then
finish_args+=(--via-pr)
else
echo "[codex-agent] Origin remote does not provide a mergeable PR surface; skipping auto-finish merge/PR pipeline." >&2
return 2
fi
finish_args+=(--via-pr)
else
echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
return 2
Expand Down Expand Up @@ -776,7 +809,10 @@ if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HE
fi
fi
else
if [[ "$final_exit" -eq 0 ]]; then
commit_status="$?"
if [[ "$commit_status" -eq 2 ]]; then
echo "[codex-agent] No sandbox changes detected on '${worktree_branch}'. Skipping auto-finish and leaving sandbox worktree in place."
elif [[ "$final_exit" -eq 0 ]]; then
final_exit=1
fi
fi
Expand Down
Loading