diff --git a/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/.openspec.yaml b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/.openspec.yaml new file mode 100644 index 0000000..4b8c565 --- /dev/null +++ b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-21 diff --git a/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/proposal.md b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/proposal.md new file mode 100644 index 0000000..6b25f79 --- /dev/null +++ b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/proposal.md @@ -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. diff --git a/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/specs/codex-agent-autofinish/spec.md b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/specs/codex-agent-autofinish/spec.md new file mode 100644 index 0000000..2690479 --- /dev/null +++ b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/specs/codex-agent-autofinish/spec.md @@ -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. diff --git a/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/tasks.md b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/tasks.md new file mode 100644 index 0000000..86c2aee --- /dev/null +++ b/openspec/changes/agent-codex-fix-codex-agent-autofinish-and-nested-gi-2026-04-21-13-28/tasks.md @@ -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 --base --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. diff --git a/scripts/codex-agent.sh b/scripts/codex-agent.sh index 37b629a..5e01f46 100755 --- a/scripts/codex-agent.sh +++ b/scripts/codex-agent.sh @@ -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:-}" @@ -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}" @@ -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" @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/templates/scripts/codex-agent.sh b/templates/scripts/codex-agent.sh index 37b629a..5e01f46 100755 --- a/templates/scripts/codex-agent.sh +++ b/templates/scripts/codex-agent.sh @@ -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:-}" @@ -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}" @@ -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" @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/test/install.test.js b/test/install.test.js index 05f68e7..a0bd173 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -111,12 +111,36 @@ function initRepoOnBranch(branchName) { } function seedCommit(repoDir) { - let result = runCmd('git', ['add', '.'], repoDir); + let result = runCmd('git', ['config', 'user.email', 'bot@example.com'], repoDir); + assert.equal(result.status, 0, result.stderr); + result = runCmd('git', ['config', 'user.name', 'Bot'], repoDir); + assert.equal(result.status, 0, result.stderr); + result = runCmd('git', ['add', '.'], repoDir); assert.equal(result.status, 0, result.stderr); result = runCmd('git', ['commit', '-m', 'seed'], repoDir); assert.equal(result.status, 0, result.stderr); } +test('seedCommit seeds local git identity for ad hoc repos', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-seed-identity-')); + const nestedRepoDir = path.join(tempDir, 'frontend'); + fs.mkdirSync(nestedRepoDir, { recursive: true }); + + let result = runCmd('git', ['init', '-b', 'main'], nestedRepoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + fs.writeFileSync(path.join(nestedRepoDir, 'package.json'), '{}\n', 'utf8'); + + seedCommit(nestedRepoDir); + + result = runCmd('git', ['config', '--local', '--get', 'user.email'], nestedRepoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.equal(result.stdout.trim(), 'bot@example.com'); + + result = runCmd('git', ['config', '--local', '--get', 'user.name'], nestedRepoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.equal(result.stdout.trim(), 'Bot'); +}); + function attachOriginRemote(repoDir) { return attachOriginRemoteForBranch(repoDir, 'dev'); } @@ -2298,6 +2322,9 @@ test('codex-agent waits for PR merge completion and cleans merged sandbox branch const ghMergeState = path.join(repoDir, '.codex-agent-gh-merge-attempts'); const { fakePath: fakeGhPath } = createFakeGhScript(` +if [[ "$1" == "auth" && "$2" == "status" ]]; then + exit 0 +fi if [[ "$1" == "pr" && "$2" == "create" ]]; then exit 0 fi