@imdeadpool/guardex 7.0.14 → 7.0.16

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.
@@ -5,11 +5,13 @@ TASK_NAME="task"
5
5
  AGENT_NAME="agent"
6
6
  BASE_BRANCH=""
7
7
  BASE_BRANCH_EXPLICIT=0
8
- WORKTREE_ROOT_REL=".omx/agent-worktrees"
8
+ WORKTREE_ROOT_REL=""
9
+ WORKTREE_ROOT_EXPLICIT=0
9
10
  OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
10
11
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
11
12
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
12
13
  OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
14
+ OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
13
15
  PRINT_NAME_ONLY=0
14
16
  POSITIONAL_ARGS=()
15
17
 
@@ -44,6 +46,7 @@ while [[ $# -gt 0 ]]; do
44
46
  ;;
45
47
  --worktree-root)
46
48
  WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
49
+ WORKTREE_ROOT_EXPLICIT=1
47
50
  shift 2
48
51
  ;;
49
52
  --)
@@ -123,12 +126,41 @@ shorten_slug() {
123
126
  printf '%s' "$shortened"
124
127
  }
125
128
 
126
- # Collapse arbitrary agent identifiers to a clean role token: claude | codex |
127
- # <other-kebab>. Priority: GUARDEX_AGENT_TYPE env override, then the raw
128
- # AGENT_NAME (if it contains 'claude' or 'codex'), then CLAUDECODE=1 sentinel
129
- # (set by Claude Code CLI), else fall back to 'codex'. Any other role name
130
- # (integrator, executor, rust-port, etc.) is preserved as-is after slug
131
- # sanitization.
129
+ env_flag_truthy() {
130
+ local raw="${1:-}"
131
+ local lowered
132
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
133
+ case "$lowered" in
134
+ 1|true|yes|on) return 0 ;;
135
+ *) return 1 ;;
136
+ esac
137
+ }
138
+
139
+ default_worktree_root_rel() {
140
+ local raw_agent="$1"
141
+ local override="${GUARDEX_AGENT_TYPE:-}"
142
+ local lowered_agent lowered_override
143
+ lowered_agent="$(printf '%s' "$raw_agent" | tr '[:upper:]' '[:lower:]')"
144
+ lowered_override="$(printf '%s' "$override" | tr '[:upper:]' '[:lower:]')"
145
+
146
+ if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" ]] || env_flag_truthy "${CLAUDECODE:-}"; then
147
+ printf '.omc/agent-worktrees'
148
+ return 0
149
+ fi
150
+
151
+ if [[ "$lowered_agent" == *claude* ]] || [[ "$lowered_override" == *claude* ]]; then
152
+ printf '.omc/agent-worktrees'
153
+ return 0
154
+ fi
155
+
156
+ printf '.omx/agent-worktrees'
157
+ }
158
+
159
+ # Collapse arbitrary agent identifiers to a clean role token. Priority:
160
+ # GUARDEX_AGENT_TYPE env override, then recognizable claude/codex aliases, then
161
+ # a small legacy compatibility set, then the literal requested role after slug
162
+ # sanitization. This preserves explicit roles such as planner/executor while
163
+ # keeping the older bot -> codex fallback stable for existing callers.
132
164
  normalize_role() {
133
165
  local raw_agent="$1"
134
166
  local override="${GUARDEX_AGENT_TYPE:-}"
@@ -150,10 +182,13 @@ normalize_role() {
150
182
  printf 'claude'
151
183
  return 0
152
184
  fi
153
- # Unrecognized raw name (rust-port-lead, some-worker, empty, ...): default to
154
- # codex. To get a different role (integrator, executor, ...) pass the role
155
- # explicitly via GUARDEX_AGENT_TYPE, handled above.
156
- printf 'codex'
185
+ local sanitized
186
+ sanitized="$(sanitize_slug "$raw_agent" "codex")"
187
+ if [[ "$sanitized" == "bot" ]]; then
188
+ printf 'codex'
189
+ return 0
190
+ fi
191
+ printf '%s' "$sanitized"
157
192
  }
158
193
 
159
194
  # Timestamp the branch/worktree/openspec slug so parallel agents never collide
@@ -192,13 +227,35 @@ normalize_bool() {
192
227
 
193
228
  OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
194
229
 
230
+ resolve_openspec_masterplan_label() {
231
+ local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
232
+ local label
233
+
234
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
235
+ printf ''
236
+ return 0
237
+ fi
238
+
239
+ label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
240
+ printf '%s' "$label"
241
+ }
242
+
195
243
  resolve_openspec_plan_slug() {
196
244
  local branch_name="$1"
197
- local task_slug="$2"
245
+ local agent_slug="$2"
246
+ local task_slug="$3"
247
+ local masterplan_label=""
248
+ local branch_leaf=""
198
249
  if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
199
250
  sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
200
251
  return 0
201
252
  fi
253
+ masterplan_label="$(resolve_openspec_masterplan_label)"
254
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
255
+ branch_leaf="${branch_name#agent/${agent_slug}/}"
256
+ sanitize_slug "agent-${agent_slug}-${masterplan_label}-${branch_leaf}" "$task_slug"
257
+ return 0
258
+ fi
202
259
  sanitize_slug "${branch_name//\//-}" "$task_slug"
203
260
  }
204
261
 
@@ -221,6 +278,22 @@ resolve_openspec_capability_slug() {
221
278
  sanitize_slug "$task_slug" "general-behavior"
222
279
  }
223
280
 
281
+ resolve_worktree_leaf() {
282
+ local branch_name="$1"
283
+ local agent_slug="$2"
284
+ local masterplan_label=""
285
+ local branch_leaf=""
286
+
287
+ masterplan_label="$(resolve_openspec_masterplan_label)"
288
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
289
+ branch_leaf="${branch_name#agent/${agent_slug}/}"
290
+ printf 'agent__%s__%s__%s' "$agent_slug" "$masterplan_label" "$branch_leaf"
291
+ return 0
292
+ fi
293
+
294
+ printf '%s' "${branch_name//\//__}"
295
+ }
296
+
224
297
  has_local_changes() {
225
298
  local root="$1"
226
299
  if ! git -C "$root" diff --quiet; then
@@ -413,6 +486,9 @@ fi
413
486
 
414
487
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
415
488
  agent_slug="$(normalize_role "$AGENT_NAME")"
489
+ if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
490
+ WORKTREE_ROOT_REL="$(default_worktree_root_rel "$AGENT_NAME")"
491
+ fi
416
492
  branch_timestamp="$(compose_branch_timestamp)"
417
493
  branch_descriptor="$(compose_branch_descriptor "$task_slug" "$branch_timestamp")"
418
494
  branch_name_base="agent/${agent_slug}/${branch_descriptor}"
@@ -460,8 +536,9 @@ done
460
536
 
461
537
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
462
538
  mkdir -p "$worktree_root"
463
- worktree_path="${worktree_root}/${branch_name//\//__}"
464
- openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
539
+ worktree_leaf="$(resolve_worktree_leaf "$branch_name" "$agent_slug")"
540
+ worktree_path="${worktree_root}/${worktree_leaf}"
541
+ openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$agent_slug" "$task_slug")"
465
542
  openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
466
543
  openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
467
544
 
@@ -497,6 +574,7 @@ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "
497
574
  exit 1
498
575
  fi
499
576
  git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
577
+ git -C "$repo_root" config "branch.${branch_name}.guardexWorktreeRoot" "$WORKTREE_ROOT_REL" >/dev/null 2>&1 || true
500
578
  # Fresh agent branches should start unpublished; clear any inherited base-branch tracking.
501
579
  git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true
502
580
 
@@ -533,4 +611,4 @@ echo "[agent-branch-start] Next steps:"
533
611
  echo " cd \"${worktree_path}\""
534
612
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
535
613
  echo " # implement + commit"
536
- echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base dev --via-pr --wait-for-merge"
614
+ echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge"
@@ -18,6 +18,10 @@ GH_BIN="${GUARDEX_GH_BIN:-gh}"
18
18
  PR_MERGED_LOOKUP_DISABLED=0
19
19
  PR_MERGED_LOOKUP_LOADED=0
20
20
  declare -A MERGED_PR_BRANCHES=()
21
+ WORKTREE_ROOT_RELS=(
22
+ ".omx/agent-worktrees"
23
+ ".omc/agent-worktrees"
24
+ )
21
25
 
22
26
  if [[ -n "$BASE_BRANCH" ]]; then
23
27
  BASE_BRANCH_EXPLICIT=1
@@ -77,13 +81,36 @@ fi
77
81
 
78
82
  repo_root="$(git rev-parse --show-toplevel)"
79
83
  current_pwd="$(pwd -P)"
80
- worktree_root="${repo_root}/.omx/agent-worktrees"
81
84
  repo_common_dir="$(
82
85
  git -C "$repo_root" rev-parse --git-common-dir \
83
86
  | awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
84
87
  )"
85
88
  repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
86
89
 
90
+ resolve_worktree_root_rel_for_entry() {
91
+ local entry="$1"
92
+ case "$entry" in
93
+ */.omc/agent-worktrees/*)
94
+ printf '%s' '.omc/agent-worktrees'
95
+ ;;
96
+ *)
97
+ printf '%s' '.omx/agent-worktrees'
98
+ ;;
99
+ esac
100
+ }
101
+
102
+ is_managed_worktree_path() {
103
+ local entry="$1"
104
+ local rel root
105
+ for rel in "${WORKTREE_ROOT_RELS[@]}"; do
106
+ root="${repo_root}/${rel}"
107
+ if [[ "$entry" == "${root}"/* ]]; then
108
+ return 0
109
+ fi
110
+ done
111
+ return 1
112
+ }
113
+
87
114
  resolve_base_branch() {
88
115
  local configured=""
89
116
  local current=""
@@ -308,54 +335,59 @@ relocated_foreign=0
308
335
  skipped_foreign=0
309
336
 
310
337
  relocate_foreign_worktree_entries() {
311
- [[ -d "$worktree_root" ]] || return 0
312
-
313
- local entry=""
314
- for entry in "${worktree_root}"/*; do
315
- [[ -d "$entry" ]] || continue
316
- if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
317
- continue
318
- fi
319
-
320
- local entry_common_dir=""
321
- entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
322
- [[ -n "$entry_common_dir" ]] || continue
338
+ local rel="" worktree_root="" entry=""
339
+ for rel in "${WORKTREE_ROOT_RELS[@]}"; do
340
+ worktree_root="${repo_root}/${rel}"
341
+ [[ -d "$worktree_root" ]] || continue
342
+
343
+ for entry in "${worktree_root}"/*; do
344
+ [[ -d "$entry" ]] || continue
345
+ if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
346
+ continue
347
+ fi
323
348
 
324
- if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
325
- continue
326
- fi
349
+ local entry_common_dir=""
350
+ entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
351
+ [[ -n "$entry_common_dir" ]] || continue
327
352
 
328
- if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
329
- skipped_foreign=$((skipped_foreign + 1))
330
- echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
331
- continue
332
- fi
353
+ if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
354
+ continue
355
+ fi
333
356
 
334
- local owner_repo_root
335
- owner_repo_root="$(dirname "$entry_common_dir")"
336
- local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
337
- local target_path
338
- target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
357
+ if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
358
+ skipped_foreign=$((skipped_foreign + 1))
359
+ echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
360
+ continue
361
+ fi
339
362
 
340
- if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
341
- skipped_foreign=$((skipped_foreign + 1))
342
- echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
343
- continue
344
- fi
363
+ local owner_repo_root
364
+ owner_repo_root="$(dirname "$entry_common_dir")"
365
+ local owner_worktree_root_rel owner_worktree_root
366
+ owner_worktree_root_rel="$(resolve_worktree_root_rel_for_entry "$entry")"
367
+ owner_worktree_root="${owner_repo_root}/${owner_worktree_root_rel}"
368
+ local target_path
369
+ target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
370
+
371
+ if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
372
+ skipped_foreign=$((skipped_foreign + 1))
373
+ echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
374
+ continue
375
+ fi
345
376
 
346
- echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
347
- if [[ "$DRY_RUN" -eq 1 ]]; then
348
- relocated_foreign=$((relocated_foreign + 1))
349
- continue
350
- fi
377
+ echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
378
+ if [[ "$DRY_RUN" -eq 1 ]]; then
379
+ relocated_foreign=$((relocated_foreign + 1))
380
+ continue
381
+ fi
351
382
 
352
- mkdir -p "$owner_worktree_root"
353
- if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
354
- relocated_foreign=$((relocated_foreign + 1))
355
- else
356
- skipped_foreign=$((skipped_foreign + 1))
357
- echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
358
- fi
383
+ mkdir -p "$owner_worktree_root"
384
+ if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
385
+ relocated_foreign=$((relocated_foreign + 1))
386
+ else
387
+ skipped_foreign=$((skipped_foreign + 1))
388
+ echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
389
+ fi
390
+ done
359
391
  done
360
392
  }
361
393
 
@@ -371,7 +403,9 @@ process_entry() {
371
403
  local branch_ref="$2"
372
404
 
373
405
  [[ -z "$wt" ]] && return
374
- [[ "$wt" != "${worktree_root}"/* ]] && return
406
+ if ! is_managed_worktree_path "$wt"; then
407
+ return
408
+ fi
375
409
 
376
410
  local branch=""
377
411
  if [[ -n "$branch_ref" ]]; then
@@ -14,6 +14,7 @@ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
14
14
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
15
15
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
16
16
  OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
17
+ OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
17
18
 
18
19
  normalize_bool() {
19
20
  local raw="${1:-}"
@@ -34,6 +35,19 @@ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
34
35
  AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
35
36
  OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
36
37
 
38
+ resolve_openspec_masterplan_label() {
39
+ local raw="${OPENSPEC_MASTERPLAN_LABEL_RAW:-}"
40
+ local label
41
+
42
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]] || [[ -z "$raw" ]]; then
43
+ printf ''
44
+ return 0
45
+ fi
46
+
47
+ label="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
48
+ printf '%s' "$label"
49
+ }
50
+
37
51
  if [[ -n "$BASE_BRANCH" ]]; then
38
52
  BASE_BRANCH_EXPLICIT=1
39
53
  fi
@@ -161,11 +175,21 @@ sanitize_slug() {
161
175
  resolve_openspec_plan_slug() {
162
176
  local branch_name="$1"
163
177
  local task_slug
178
+ local masterplan_label=""
179
+ local branch_role=""
180
+ local branch_leaf=""
164
181
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
165
182
  if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
166
183
  sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
167
184
  return 0
168
185
  fi
186
+ masterplan_label="$(resolve_openspec_masterplan_label)"
187
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
188
+ branch_role="${BASH_REMATCH[1]}"
189
+ branch_leaf="${BASH_REMATCH[2]}"
190
+ sanitize_slug "agent-${branch_role}-${masterplan_label}-${branch_leaf}" "$task_slug"
191
+ return 0
192
+ fi
169
193
  sanitize_slug "${branch_name//\//-}" "$task_slug"
170
194
  }
171
195
 
@@ -190,6 +214,23 @@ resolve_openspec_capability_slug() {
190
214
  sanitize_slug "$task_slug" "general-behavior"
191
215
  }
192
216
 
217
+ resolve_worktree_leaf() {
218
+ local branch_name="$1"
219
+ local masterplan_label=""
220
+ local branch_role=""
221
+ local branch_leaf=""
222
+
223
+ masterplan_label="$(resolve_openspec_masterplan_label)"
224
+ if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
225
+ branch_role="${BASH_REMATCH[1]}"
226
+ branch_leaf="${BASH_REMATCH[2]}"
227
+ printf 'agent__%s__%s__%s' "$branch_role" "$masterplan_label" "$branch_leaf"
228
+ return 0
229
+ fi
230
+
231
+ printf '%s' "${branch_name//\//__}"
232
+ }
233
+
193
234
  hydrate_local_helper_in_worktree() {
194
235
  local worktree="$1"
195
236
  local relative_path="$2"
@@ -249,6 +290,35 @@ resolve_start_ref() {
249
290
  return 1
250
291
  }
251
292
 
293
+ origin_remote_looks_like_github() {
294
+ local wt="$1"
295
+ local origin_url=""
296
+ origin_url="$(git -C "$wt" remote get-url origin 2>/dev/null || true)"
297
+ [[ -n "$origin_url" && "$origin_url" =~ github\.com[:/] ]]
298
+ }
299
+
300
+ auto_finish_context_is_ready() {
301
+ local wt="$1"
302
+ local gh_bin="${GUARDEX_GH_BIN:-gh}"
303
+
304
+ if ! git -C "$wt" remote get-url origin >/dev/null 2>&1; then
305
+ return 1
306
+ fi
307
+ if ! command -v "$gh_bin" >/dev/null 2>&1; then
308
+ return 1
309
+ fi
310
+
311
+ if [[ -n "${GUARDEX_GH_BIN:-}" ]]; then
312
+ return 0
313
+ fi
314
+
315
+ if ! origin_remote_looks_like_github "$wt"; then
316
+ return 1
317
+ fi
318
+
319
+ "$gh_bin" auth status >/dev/null 2>&1
320
+ }
321
+
252
322
  restore_repo_branch_if_changed() {
253
323
  local expected_branch="$1"
254
324
  if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
@@ -285,7 +355,7 @@ start_sandbox_fallback() {
285
355
 
286
356
  worktree_root="${repo_root}/.omx/agent-worktrees"
287
357
  mkdir -p "$worktree_root"
288
- worktree_path="${worktree_root}/${branch_name//\//__}"
358
+ worktree_path="${worktree_root}/$(resolve_worktree_leaf "$branch_name")"
289
359
  if [[ -e "$worktree_path" ]]; then
290
360
  echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
291
361
  return 1
@@ -317,7 +387,11 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
317
387
  start_output=""
318
388
  start_status=0
319
389
  set +e
320
- start_output="$(GUARDEX_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
390
+ start_output="$(
391
+ GUARDEX_OPENSPEC_AUTO_INIT="$OPENSPEC_AUTO_INIT" \
392
+ GUARDEX_OPENSPEC_MASTERPLAN_LABEL="$OPENSPEC_MASTERPLAN_LABEL_RAW" \
393
+ bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1
394
+ )"
321
395
  start_status=$?
322
396
  set -e
323
397
 
@@ -372,6 +446,17 @@ has_origin_remote() {
372
446
  git -C "$repo_root" remote get-url origin >/dev/null 2>&1
373
447
  }
374
448
 
449
+ origin_remote_supports_pr_finish() {
450
+ local origin_url
451
+ origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
452
+ case "$origin_url" in
453
+ ''|/*|./*|../*|file://*)
454
+ return 1
455
+ ;;
456
+ esac
457
+ return 0
458
+ }
459
+
375
460
  resolve_worktree_base_branch() {
376
461
  local _wt="$1"
377
462
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
@@ -685,7 +770,12 @@ run_finish_flow() {
685
770
  echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${GUARDEX_GH_BIN:-gh}" >&2
686
771
  return 2
687
772
  fi
688
- finish_args+=(--via-pr)
773
+ if origin_remote_supports_pr_finish; then
774
+ finish_args+=(--via-pr)
775
+ else
776
+ echo "[codex-agent] Origin remote does not provide a mergeable PR surface; skipping auto-finish merge/PR pipeline." >&2
777
+ return 2
778
+ fi
689
779
  else
690
780
  echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
691
781
  return 2
@@ -764,7 +854,9 @@ if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HE
764
854
  else
765
855
  echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
766
856
  fi
767
- if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
857
+ if ! auto_finish_context_is_ready "$worktree_path"; then
858
+ echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
859
+ elif auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
768
860
  if run_finish_flow "$worktree_path" "$worktree_branch"; then
769
861
  auto_finish_completed=1
770
862
  echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ compose_file="${GUARDEX_DOCKER_COMPOSE_FILE:-}"
6
+ service="${GUARDEX_DOCKER_SERVICE:-}"
7
+ mode="${GUARDEX_DOCKER_MODE:-auto}"
8
+ workdir_override="${GUARDEX_DOCKER_WORKDIR:-}"
9
+
10
+ usage() {
11
+ cat >&2 <<'EOF'
12
+ Usage: bash scripts/guardex-docker-loader.sh [--] <command...>
13
+
14
+ Environment:
15
+ GUARDEX_DOCKER_SERVICE=<compose-service> required unless compose defines exactly one service
16
+ GUARDEX_DOCKER_COMPOSE_FILE=<path> optional docker compose file override
17
+ GUARDEX_DOCKER_MODE=auto|exec|run default: auto
18
+ GUARDEX_DOCKER_WORKDIR=<path> optional working directory override inside the container
19
+ EOF
20
+ }
21
+
22
+ choose_compose_cmd() {
23
+ if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
24
+ printf 'docker compose'
25
+ return 0
26
+ fi
27
+ if command -v docker-compose >/dev/null 2>&1; then
28
+ printf 'docker-compose'
29
+ return 0
30
+ fi
31
+ return 1
32
+ }
33
+
34
+ mapfile_from_lines() {
35
+ local raw="$1"
36
+ local -n out_ref="$2"
37
+ out_ref=()
38
+ while IFS= read -r line; do
39
+ [[ -n "$line" ]] || continue
40
+ out_ref+=("$line")
41
+ done <<<"$raw"
42
+ }
43
+
44
+ if [[ "${1:-}" == "--" ]]; then
45
+ shift
46
+ fi
47
+
48
+ if [[ $# -eq 0 ]]; then
49
+ usage
50
+ exit 1
51
+ fi
52
+
53
+ if [[ "$mode" != "auto" && "$mode" != "exec" && "$mode" != "run" ]]; then
54
+ echo "[guardex-docker-loader] Invalid GUARDEX_DOCKER_MODE: $mode" >&2
55
+ usage
56
+ exit 1
57
+ fi
58
+
59
+ compose_cmd_raw="$(choose_compose_cmd)" || {
60
+ echo "[guardex-docker-loader] Docker Compose is not available. Install docker compose or docker-compose first." >&2
61
+ exit 1
62
+ }
63
+ IFS=' ' read -r -a compose_cmd <<<"$compose_cmd_raw"
64
+ compose_args=()
65
+ if [[ -n "$compose_file" ]]; then
66
+ compose_args=(-f "$compose_file")
67
+ fi
68
+
69
+ cd "$repo_root"
70
+
71
+ services_raw="$("${compose_cmd[@]}" "${compose_args[@]}" config --services 2>/dev/null || true)"
72
+ declare -a services
73
+ mapfile_from_lines "$services_raw" services
74
+ if [[ ${#services[@]} -eq 0 ]]; then
75
+ echo "[guardex-docker-loader] No Docker Compose services found. Add a compose file or set GUARDEX_DOCKER_COMPOSE_FILE." >&2
76
+ exit 1
77
+ fi
78
+
79
+ if [[ -z "$service" ]]; then
80
+ if [[ ${#services[@]} -eq 1 ]]; then
81
+ service="${services[0]}"
82
+ else
83
+ echo "[guardex-docker-loader] Multiple services found (${services[*]}). Set GUARDEX_DOCKER_SERVICE." >&2
84
+ exit 1
85
+ fi
86
+ fi
87
+
88
+ service_known=0
89
+ for candidate in "${services[@]}"; do
90
+ if [[ "$candidate" == "$service" ]]; then
91
+ service_known=1
92
+ break
93
+ fi
94
+ done
95
+ if [[ $service_known -ne 1 ]]; then
96
+ echo "[guardex-docker-loader] Compose service not found: $service" >&2
97
+ exit 1
98
+ fi
99
+
100
+ run_mode="$mode"
101
+ if [[ "$run_mode" == "auto" ]]; then
102
+ run_mode="run"
103
+ running_raw="$("${compose_cmd[@]}" "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
104
+ declare -a running_services
105
+ mapfile_from_lines "$running_raw" running_services
106
+ for candidate in "${running_services[@]}"; do
107
+ if [[ "$candidate" == "$service" ]]; then
108
+ run_mode="exec"
109
+ break
110
+ fi
111
+ done
112
+ fi
113
+
114
+ workdir_args=()
115
+ if [[ -n "$workdir_override" ]]; then
116
+ workdir_args=(-w "$workdir_override")
117
+ fi
118
+
119
+ if [[ "$run_mode" == "exec" ]]; then
120
+ exec "${compose_cmd[@]}" "${compose_args[@]}" exec -T "${workdir_args[@]}" "$service" "$@"
121
+ fi
122
+
123
+ exec "${compose_cmd[@]}" "${compose_args[@]}" run --rm -T "${workdir_args[@]}" "$service" "$@"