@imdeadpool/guardex 6.1.0 → 7.0.1

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.
@@ -6,11 +6,33 @@ AGENT_NAME="${GUARDEX_AGENT_NAME:-agent}"
6
6
  BASE_BRANCH="${GUARDEX_BASE_BRANCH:-}"
7
7
  BASE_BRANCH_EXPLICIT=0
8
8
  CODEX_BIN="${GUARDEX_CODEX_BIN:-codex}"
9
- GH_PR_REF="${GUARDEX_GH_PR_REF:-}"
10
- GH_REPO_REF="${GUARDEX_GH_REPO:-}"
11
- GH_SYNC_FLAG=""
12
- ROLE=""
13
- CODEX_EXTRA_ARGS=()
9
+ AUTO_FINISH_RAW="${GUARDEX_CODEX_AUTO_FINISH:-true}"
10
+ AUTO_REVIEW_ON_CONFLICT_RAW="${GUARDEX_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
11
+ AUTO_CLEANUP_RAW="${GUARDEX_CODEX_AUTO_CLEANUP:-true}"
12
+ AUTO_WAIT_FOR_MERGE_RAW="${GUARDEX_CODEX_WAIT_FOR_MERGE:-true}"
13
+ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
14
+ OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
15
+ OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
16
+ OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
17
+
18
+ normalize_bool() {
19
+ local raw="${1:-}"
20
+ local fallback="${2:-0}"
21
+ local lowered
22
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
23
+ case "$lowered" in
24
+ 1|true|yes|on) printf '1' ;;
25
+ 0|false|no|off) printf '0' ;;
26
+ '') printf '%s' "$fallback" ;;
27
+ *) printf '%s' "$fallback" ;;
28
+ esac
29
+ }
30
+
31
+ AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
32
+ AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"
33
+ AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
34
+ AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
35
+ OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
14
36
 
15
37
  if [[ -n "$BASE_BRANCH" ]]; then
16
38
  BASE_BRANCH_EXPLICIT=1
@@ -35,25 +57,37 @@ while [[ $# -gt 0 ]]; do
35
57
  CODEX_BIN="${2:-$CODEX_BIN}"
36
58
  shift 2
37
59
  ;;
38
- --pr)
39
- GH_PR_REF="${2:-$GH_PR_REF}"
40
- shift 2
60
+ --auto-finish)
61
+ AUTO_FINISH=1
62
+ shift
41
63
  ;;
42
- --repo)
43
- GH_REPO_REF="${2:-$GH_REPO_REF}"
44
- shift 2
64
+ --no-auto-finish)
65
+ AUTO_FINISH=0
66
+ shift
45
67
  ;;
46
- --gh-sync)
47
- GH_SYNC_FLAG="--gh-sync"
68
+ --auto-review-on-conflict)
69
+ AUTO_REVIEW_ON_CONFLICT=1
48
70
  shift
49
71
  ;;
50
- --no-gh-sync)
51
- GH_SYNC_FLAG="--no-gh-sync"
72
+ --no-auto-review-on-conflict)
73
+ AUTO_REVIEW_ON_CONFLICT=0
52
74
  shift
53
75
  ;;
54
- --role)
55
- ROLE="${2:-$ROLE}"
56
- shift 2
76
+ --cleanup)
77
+ AUTO_CLEANUP=1
78
+ shift
79
+ ;;
80
+ --no-cleanup)
81
+ AUTO_CLEANUP=0
82
+ shift
83
+ ;;
84
+ --wait-for-merge)
85
+ AUTO_WAIT_FOR_MERGE=1
86
+ shift
87
+ ;;
88
+ --no-wait-for-merge)
89
+ AUTO_WAIT_FOR_MERGE=0
90
+ shift
57
91
  ;;
58
92
  --)
59
93
  shift
@@ -96,6 +130,160 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
96
130
  fi
97
131
  repo_root="$(git rev-parse --show-toplevel)"
98
132
 
133
+ sanitize_slug() {
134
+ local raw="$1"
135
+ local fallback="${2:-task}"
136
+ local slug
137
+ slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
138
+ if [[ -z "$slug" ]]; then
139
+ slug="$fallback"
140
+ fi
141
+ printf '%s' "$slug"
142
+ }
143
+
144
+ resolve_openspec_plan_slug() {
145
+ local branch_name="$1"
146
+ local task_slug
147
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
148
+ if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
149
+ sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
150
+ return 0
151
+ fi
152
+ sanitize_slug "${branch_name//\//-}" "$task_slug"
153
+ }
154
+
155
+ resolve_openspec_change_slug() {
156
+ local branch_name="$1"
157
+ local task_slug
158
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
159
+ if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then
160
+ sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug"
161
+ return 0
162
+ fi
163
+ sanitize_slug "${branch_name//\//-}" "$task_slug"
164
+ }
165
+
166
+ resolve_openspec_capability_slug() {
167
+ local task_slug
168
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
169
+ if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then
170
+ sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug"
171
+ return 0
172
+ fi
173
+ sanitize_slug "$task_slug" "general-behavior"
174
+ }
175
+
176
+ hydrate_local_helper_in_worktree() {
177
+ local worktree="$1"
178
+ local relative_path="$2"
179
+ local worktree_target="${worktree}/${relative_path}"
180
+ local source_path=""
181
+
182
+ if [[ -e "$worktree_target" ]]; then
183
+ return 0
184
+ fi
185
+
186
+ if [[ -f "${repo_root}/${relative_path}" ]]; then
187
+ source_path="${repo_root}/${relative_path}"
188
+ elif [[ -f "${repo_root}/templates/${relative_path}" ]]; then
189
+ source_path="${repo_root}/templates/${relative_path}"
190
+ fi
191
+
192
+ if [[ -z "$source_path" ]]; then
193
+ return 0
194
+ fi
195
+
196
+ mkdir -p "$(dirname "$worktree_target")"
197
+ cp "$source_path" "$worktree_target"
198
+ if [[ -x "$source_path" ]]; then
199
+ chmod +x "$worktree_target"
200
+ fi
201
+
202
+ echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}"
203
+ }
204
+
205
+ resolve_start_base_branch() {
206
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
207
+ printf '%s' "$BASE_BRANCH"
208
+ return 0
209
+ fi
210
+
211
+ local configured_base
212
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
213
+ if [[ -n "$configured_base" ]]; then
214
+ printf '%s' "$configured_base"
215
+ return 0
216
+ fi
217
+
218
+ printf 'dev'
219
+ }
220
+
221
+ resolve_start_ref() {
222
+ local base_branch="$1"
223
+ git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
224
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
225
+ printf 'origin/%s' "$base_branch"
226
+ return 0
227
+ fi
228
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
229
+ printf '%s' "$base_branch"
230
+ return 0
231
+ fi
232
+ return 1
233
+ }
234
+
235
+ restore_repo_branch_if_changed() {
236
+ local expected_branch="$1"
237
+ if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
238
+ return 0
239
+ fi
240
+ local current_branch
241
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
242
+ if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
243
+ return 0
244
+ fi
245
+ git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
246
+ }
247
+
248
+ start_sandbox_fallback() {
249
+ local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
250
+ local worktree_root worktree_path
251
+
252
+ base_branch="$(resolve_start_base_branch)"
253
+ if ! start_ref="$(resolve_start_ref "$base_branch")"; then
254
+ echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
255
+ return 1
256
+ fi
257
+
258
+ timestamp="$(date +%Y%m%d-%H%M%S)"
259
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
260
+ agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
261
+ branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
262
+ branch_name="$branch_name_base"
263
+ suffix=2
264
+ while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
265
+ branch_name="${branch_name_base}-${suffix}"
266
+ suffix=$((suffix + 1))
267
+ done
268
+
269
+ worktree_root="${repo_root}/.omx/agent-worktrees"
270
+ mkdir -p "$worktree_root"
271
+ worktree_path="${worktree_root}/${branch_name//\//__}"
272
+ if [[ -e "$worktree_path" ]]; then
273
+ echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
274
+ return 1
275
+ fi
276
+
277
+ git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
278
+ git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$base_branch" >/dev/null 2>&1 || true
279
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
280
+ git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
281
+ fi
282
+
283
+ printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
284
+ printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
285
+ }
286
+
99
287
  if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
100
288
  echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
101
289
  exit 1
@@ -105,291 +293,489 @@ start_args=("$TASK_NAME" "$AGENT_NAME")
105
293
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
106
294
  start_args+=("$BASE_BRANCH")
107
295
  fi
108
- if [[ -n "$GH_PR_REF" ]]; then
109
- start_args+=(--pr "$GH_PR_REF")
296
+
297
+ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
298
+ start_output=""
299
+ start_status=0
300
+ set +e
301
+ start_output="$(GUARDEX_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
302
+ start_status=$?
303
+ set -e
304
+
305
+ worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
306
+ current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
307
+ resolved_repo_root="$(cd "$repo_root" && pwd -P)"
308
+ resolved_worktree_path=""
309
+ if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
310
+ resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
311
+ fi
312
+
313
+ fallback_reason=""
314
+ if [[ "$start_status" -ne 0 ]]; then
315
+ fallback_reason="starter exited with status ${start_status}"
316
+ elif [[ -z "$worktree_path" ]]; then
317
+ fallback_reason="starter did not report worktree path"
318
+ elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
319
+ fallback_reason="starter pointed to active checkout path"
320
+ elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
321
+ fallback_reason="starter switched active checkout branch"
110
322
  fi
111
- if [[ -n "$GH_REPO_REF" ]]; then
112
- start_args+=(--repo "$GH_REPO_REF")
323
+
324
+ if [[ -n "$fallback_reason" ]]; then
325
+ if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
326
+ echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
327
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
328
+ exit 1
329
+ fi
330
+ if [[ -n "$start_output" ]]; then
331
+ printf '%s\n' "$start_output" >&2
332
+ fi
333
+ echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
334
+ start_output="$(start_sandbox_fallback)"
335
+ printf '%s\n' "$start_output"
336
+ worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
337
+ else
338
+ printf '%s\n' "$start_output"
113
339
  fi
114
- if [[ -n "$GH_SYNC_FLAG" ]]; then
115
- start_args+=("$GH_SYNC_FLAG")
340
+
341
+ if [[ -z "$worktree_path" ]]; then
342
+ echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
343
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
344
+ exit 1
116
345
  fi
117
346
 
118
- derive_worktree_session_key() {
119
- local worktree="$1"
120
- local digest=""
347
+ if [[ ! -d "$worktree_path" ]]; then
348
+ echo "[codex-agent] Reported worktree path does not exist: $worktree_path" >&2
349
+ exit 1
350
+ fi
351
+
352
+ has_origin_remote() {
353
+ git -C "$repo_root" remote get-url origin >/dev/null 2>&1
354
+ }
355
+
356
+ resolve_worktree_base_branch() {
357
+ local _wt="$1"
358
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
359
+ printf '%s' "$BASE_BRANCH"
360
+ return 0
361
+ fi
362
+
363
+ local configured_base
364
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
365
+ if [[ -n "$configured_base" ]]; then
366
+ printf '%s' "$configured_base"
367
+ return 0
368
+ fi
369
+
370
+ printf 'dev'
371
+ }
121
372
 
122
- if command -v sha256sum >/dev/null 2>&1; then
123
- digest="$(printf '%s' "$worktree" | sha256sum | awk '{print $1}' | cut -c1-20)"
124
- elif command -v shasum >/dev/null 2>&1; then
125
- digest="$(printf '%s' "$worktree" | shasum -a 256 | awk '{print $1}' | cut -c1-20)"
373
+ sync_worktree_with_base() {
374
+ local wt="$1"
375
+ if ! has_origin_remote; then
376
+ return 0
126
377
  fi
127
378
 
128
- if [[ -z "$digest" ]]; then
129
- digest="$(printf '%s' "$worktree" | tr -cs 'a-zA-Z0-9' '-' | sed -E 's/^-+//; s/-+$//' | cut -c1-40)"
379
+ local base_branch
380
+ base_branch="$(resolve_worktree_base_branch "$wt")"
381
+ if [[ -z "$base_branch" ]]; then
382
+ return 0
130
383
  fi
131
384
 
132
- if [[ -z "$digest" ]]; then
133
- digest="sandbox"
385
+ if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
386
+ echo "[codex-agent] Warning: could not fetch origin/${base_branch} before task start." >&2
387
+ return 0
134
388
  fi
135
389
 
136
- printf 'worktree:%s' "$digest"
390
+ if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
391
+ return 0
392
+ fi
393
+
394
+ local behind_count
395
+ behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
396
+ behind_count="${behind_count:-0}"
397
+ if [[ "$behind_count" -le 0 ]]; then
398
+ return 0
399
+ fi
400
+
401
+ local branch
402
+ branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
403
+ echo "[codex-agent] Task sync: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Rebasing before launch..."
404
+ if ! git -C "$wt" rebase "origin/${base_branch}"; then
405
+ echo "[codex-agent] Task sync failed. Resolve and continue in sandbox:" >&2
406
+ echo " git -C \"$wt\" rebase --continue" >&2
407
+ echo " # or abort" >&2
408
+ echo " git -C \"$wt\" rebase --abort" >&2
409
+ return 1
410
+ fi
411
+ echo "[codex-agent] Task sync complete."
412
+ return 0
137
413
  }
138
414
 
139
- export_worktree_mem0_env() {
140
- local worktree="$1"
141
- local mem0_dir="${worktree}/.omx/mem0"
142
- local notepad_path="${mem0_dir}/notepad.md"
143
- local project_memory_path="${mem0_dir}/project-memory.json"
144
- local scope_path="${mem0_dir}/worktree-scope.json"
415
+ ensure_openspec_plan_workspace() {
416
+ local wt="$1"
417
+ local branch="$2"
418
+
419
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
420
+ return 0
421
+ fi
422
+
423
+ hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh"
424
+
425
+ local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh"
426
+ if [[ ! -f "$openspec_script" ]]; then
427
+ echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2
428
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
429
+ return 1
430
+ fi
431
+ if [[ ! -x "$openspec_script" ]]; then
432
+ chmod +x "$openspec_script" 2>/dev/null || true
433
+ fi
434
+
435
+ local plan_slug
436
+ plan_slug="$(resolve_openspec_plan_slug "$branch")"
437
+ local init_output=""
438
+ if ! init_output="$(
439
+ cd "$wt"
440
+ bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
441
+ )"; then
442
+ printf '%s\n' "$init_output" >&2
443
+ echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
444
+ return 1
445
+ fi
446
+ if [[ -n "$init_output" ]]; then
447
+ printf '%s\n' "$init_output"
448
+ fi
449
+ echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}"
450
+ }
451
+
452
+ ensure_openspec_change_workspace() {
453
+ local wt="$1"
454
+ local branch="$2"
145
455
 
146
- export OMX_MEM0_SCOPE="worktree"
147
- export OMX_MEM0_DIR="$mem0_dir"
148
- if [[ -f "$notepad_path" ]]; then
149
- export OMX_NOTEPAD_PATH="$notepad_path"
456
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
457
+ return 0
150
458
  fi
151
- if [[ -f "$project_memory_path" ]]; then
152
- export OMX_PROJECT_MEMORY_PATH="$project_memory_path"
459
+
460
+ hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-change-workspace.sh"
461
+
462
+ local openspec_script="${wt}/scripts/openspec/init-change-workspace.sh"
463
+ if [[ ! -f "$openspec_script" ]]; then
464
+ echo "[codex-agent] Missing OpenSpec change init script in sandbox: ${openspec_script}" >&2
465
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
466
+ return 1
153
467
  fi
154
- if [[ -f "$scope_path" ]]; then
155
- export OMX_MEM0_SCOPE_PATH="$scope_path"
468
+ if [[ ! -x "$openspec_script" ]]; then
469
+ chmod +x "$openspec_script" 2>/dev/null || true
156
470
  fi
157
471
 
158
- if [[ -z "${CODEX_AUTH_SESSION_KEY:-}" ]]; then
159
- export CODEX_AUTH_SESSION_KEY="$(derive_worktree_session_key "$worktree")"
160
- echo "[codex-agent] Scoped CODEX_AUTH_SESSION_KEY to ${CODEX_AUTH_SESSION_KEY}"
472
+ local change_slug capability_slug init_output=""
473
+ change_slug="$(resolve_openspec_change_slug "$branch")"
474
+ capability_slug="$(resolve_openspec_capability_slug)"
475
+ if ! init_output="$(
476
+ cd "$wt"
477
+ bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
478
+ )"; then
479
+ printf '%s\n' "$init_output" >&2
480
+ echo "[codex-agent] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
481
+ return 1
482
+ fi
483
+ if [[ -n "$init_output" ]]; then
484
+ printf '%s\n' "$init_output"
161
485
  fi
486
+ echo "[codex-agent] OpenSpec change workspace: ${wt}/openspec/changes/${change_slug}"
487
+ }
162
488
 
163
- echo "[codex-agent] Worktree mem0 scope: $mem0_dir"
489
+ worktree_has_changes() {
490
+ local wt="$1"
491
+ if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
492
+ return 0
493
+ fi
494
+ if ! git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
495
+ return 0
496
+ fi
497
+ if [[ -n "$(git -C "$wt" ls-files --others --exclude-standard)" ]]; then
498
+ return 0
499
+ fi
500
+ return 1
164
501
  }
165
502
 
166
- resolve_finish_base_branch() {
167
- local branch="$1"
168
- local stored_base=""
503
+ claim_changed_files() {
504
+ local wt="$1"
505
+ local branch="$2"
506
+ local lock_script="${repo_root}/scripts/agent-file-locks.py"
169
507
 
170
- stored_base="$(git -C "$repo_root" config --get "branch.${branch}.guardexBase" || true)"
171
- if [[ -n "$stored_base" ]]; then
172
- printf '%s' "$stored_base"
173
- return
508
+ if [[ ! -x "$lock_script" ]]; then
509
+ return 0
174
510
  fi
175
511
 
176
- if [[ -n "$BASE_BRANCH" ]]; then
177
- printf '%s' "$BASE_BRANCH"
512
+ local changed_raw deleted_raw
513
+ changed_raw="$({
514
+ git -C "$wt" diff --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
515
+ git -C "$wt" diff --cached --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
516
+ git -C "$wt" ls-files --others --exclude-standard;
517
+ } | sed '/^$/d' | sort -u)"
518
+
519
+ if [[ -n "$changed_raw" ]]; then
520
+ mapfile -t changed_files < <(printf '%s\n' "$changed_raw")
521
+ python3 "$lock_script" claim --branch "$branch" "${changed_files[@]}" >/dev/null 2>&1 || true
522
+ fi
523
+
524
+ deleted_raw="$({
525
+ git -C "$wt" diff --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
526
+ git -C "$wt" diff --cached --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
527
+ } | sed '/^$/d' | sort -u)"
528
+
529
+ if [[ -n "$deleted_raw" ]]; then
530
+ mapfile -t deleted_files < <(printf '%s\n' "$deleted_raw")
531
+ python3 "$lock_script" allow-delete --branch "$branch" "${deleted_files[@]}" >/dev/null 2>&1 || true
178
532
  fi
179
533
  }
180
534
 
181
- render_finish_hint() {
182
- local branch="$1"
183
- local base="${2:-}"
184
- local hint=""
535
+ auto_commit_worktree_changes() {
536
+ local wt="$1"
537
+ local branch="$2"
185
538
 
186
- hint="bash scripts/agent-branch-finish.sh --branch \"${branch}\""
187
- if [[ -n "$base" ]]; then
188
- hint="${hint} --base \"${base}\""
539
+ if ! worktree_has_changes "$wt"; then
540
+ return 0
189
541
  fi
190
- hint="${hint} --via-pr --wait-for-merge --cleanup"
191
- if [[ -n "$GH_PR_REF" ]]; then
192
- hint="${hint} --pr \"${GH_PR_REF}\""
542
+
543
+ claim_changed_files "$wt" "$branch"
544
+ git -C "$wt" add -A
545
+
546
+ if git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
547
+ return 0
193
548
  fi
194
- if [[ -n "$GH_REPO_REF" ]]; then
195
- hint="${hint} --repo \"${GH_REPO_REF}\""
549
+
550
+ local default_message="Auto-finish: ${TASK_NAME}"
551
+ local commit_message="${GUARDEX_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
552
+ local commit_output=""
553
+
554
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
555
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
556
+ return 0
196
557
  fi
197
558
 
198
- printf '%s' "$hint"
559
+ if auto_sync_for_commit_retry "$wt" "$branch"; then
560
+ claim_changed_files "$wt" "$branch"
561
+ git -C "$wt" add -A
562
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
563
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}' after sync retry."
564
+ return 0
565
+ fi
566
+ fi
567
+
568
+ echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
569
+ if [[ -n "$commit_output" ]]; then
570
+ printf '%s\n' "$commit_output" >&2
571
+ fi
572
+ return 1
199
573
  }
200
574
 
201
- resolve_role_model() {
202
- local role="$1"
203
- local config_path="$2"
204
- python3 - "$role" "$config_path" <<'PY'
205
- import json
206
- import os
207
- import re
208
- import sys
209
-
210
- role = sys.argv[1]
211
- config_path = sys.argv[2]
212
- if not os.path.exists(config_path):
213
- print(f"codex-agent: config file not found: {config_path}", file=sys.stderr)
214
- sys.exit(2)
215
- try:
216
- with open(config_path, "r", encoding="utf-8") as fh:
217
- cfg = json.load(fh)
218
- except json.JSONDecodeError as exc:
219
- print(f"codex-agent: config file is not valid JSON ({config_path}): {exc}", file=sys.stderr)
220
- sys.exit(2)
221
- if not isinstance(cfg, dict):
222
- print(f"codex-agent: config file root is not a JSON object: {config_path}", file=sys.stderr)
223
- sys.exit(2)
224
- for key in ("version", "tiers", "roles"):
225
- if key not in cfg:
226
- print(
227
- f"codex-agent: config file missing required top-level key '{key}': {config_path}",
228
- file=sys.stderr,
229
- )
230
- sys.exit(2)
231
- tiers = cfg.get("tiers") or {}
232
- roles = cfg.get("roles") or {}
233
- if not isinstance(tiers, dict) or not tiers:
234
- print(f"codex-agent: config 'tiers' must be a non-empty object: {config_path}", file=sys.stderr)
235
- sys.exit(2)
236
- if not isinstance(roles, dict) or not roles:
237
- print(f"codex-agent: config 'roles' must be a non-empty object: {config_path}", file=sys.stderr)
238
- sys.exit(2)
239
- if role not in roles:
240
- valid = ", ".join(sorted(roles.keys()))
241
- print(
242
- f"codex-agent: role '{role}' is not defined in {config_path}. Valid roles: {valid}",
243
- file=sys.stderr,
244
- )
245
- sys.exit(2)
246
- role_entry = roles[role]
247
- if not isinstance(role_entry, dict) or "tier" not in role_entry:
248
- print(f"codex-agent: role '{role}' entry is missing 'tier' in {config_path}", file=sys.stderr)
249
- sys.exit(2)
250
- tier_name = role_entry["tier"]
251
- if tier_name not in tiers:
252
- print(
253
- f"codex-agent: role '{role}' points to tier '{tier_name}' which is not defined in {config_path}",
254
- file=sys.stderr,
255
- )
256
- sys.exit(2)
257
- tier_entry = tiers[tier_name]
258
- if not isinstance(tier_entry, dict) or not tier_entry.get("model"):
259
- print(
260
- f"codex-agent: tier '{tier_name}' is missing a non-empty 'model' field in {config_path}",
261
- file=sys.stderr,
262
- )
263
- sys.exit(2)
264
- model = tier_entry["model"]
265
- if re.fullmatch(r"REPLACE_WITH_[A-Z_]+_MODEL_ID", model):
266
- print(
267
- f"codex-agent: tier '{tier_name}' still has a placeholder model ID. "
268
- f"Edit {config_path} and replace {model}.",
269
- file=sys.stderr,
575
+ auto_sync_for_commit_retry() {
576
+ local wt="$1"
577
+ local branch="$2"
578
+
579
+ if ! has_origin_remote; then
580
+ return 1
581
+ fi
582
+
583
+ local base_branch
584
+ base_branch="$(resolve_worktree_base_branch "$wt")"
585
+ if [[ -z "$base_branch" ]]; then
586
+ return 1
587
+ fi
588
+
589
+ if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
590
+ return 1
591
+ fi
592
+
593
+ if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
594
+ return 1
595
+ fi
596
+
597
+ local behind_count
598
+ behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
599
+ behind_count="${behind_count:-0}"
600
+ if [[ "$behind_count" -le 0 ]]; then
601
+ return 1
602
+ fi
603
+
604
+ echo "[codex-agent] Auto-commit retry: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Syncing and retrying..."
605
+
606
+ local stash_ref=""
607
+ local stash_output=""
608
+ if worktree_has_changes "$wt"; then
609
+ if ! stash_output="$(git -C "$wt" stash push --include-untracked -m "codex-agent-autocommit-sync-${branch}-$(date +%s)" 2>&1)"; then
610
+ return 1
611
+ fi
612
+ stash_ref="$(printf '%s\n' "$stash_output" | grep -o 'stash@{[0-9]\+}' | head -n 1 || true)"
613
+ fi
614
+
615
+ if ! git -C "$wt" rebase "origin/${base_branch}" >/dev/null 2>&1; then
616
+ git -C "$wt" rebase --abort >/dev/null 2>&1 || true
617
+ if [[ -n "$stash_ref" ]]; then
618
+ git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1 || true
619
+ fi
620
+ return 1
621
+ fi
622
+
623
+ if [[ -n "$stash_ref" ]]; then
624
+ if ! git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1; then
625
+ echo "[codex-agent] Auto-commit retry could not re-apply local changes after sync. Manual resolution required in: $wt" >&2
626
+ return 1
627
+ fi
628
+ fi
629
+
630
+ return 0
631
+ }
632
+
633
+ looks_like_conflict_failure() {
634
+ local output="$1"
635
+ if grep -qiE 'preflight conflict detected|merge conflict detected|auto-sync failed while rebasing|rebase --continue|rebase --abort' <<< "$output"; then
636
+ return 0
637
+ fi
638
+ return 1
639
+ }
640
+
641
+ run_finish_flow() {
642
+ local wt="$1"
643
+ local branch="$2"
644
+ local finish_base_branch=""
645
+ local finish_output=""
646
+ local -a finish_args
647
+
648
+ finish_args=(--branch "$branch")
649
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
650
+ finish_base_branch="$BASE_BRANCH"
651
+ else
652
+ finish_base_branch="$(resolve_worktree_base_branch "$wt")"
653
+ fi
654
+ if [[ -n "$finish_base_branch" ]]; then
655
+ finish_args+=(--base "$finish_base_branch")
656
+ fi
657
+ if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
658
+ finish_args+=(--cleanup)
659
+ fi
660
+ if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 ]]; then
661
+ finish_args+=(--wait-for-merge)
662
+ fi
663
+
664
+ if has_origin_remote; then
665
+ if ! command -v "${GUARDEX_GH_BIN:-gh}" >/dev/null 2>&1 && ! command -v gh >/dev/null 2>&1; then
666
+ echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${GUARDEX_GH_BIN:-gh}" >&2
667
+ return 2
668
+ fi
669
+ finish_args+=(--via-pr)
670
+ else
671
+ echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
672
+ return 2
673
+ fi
674
+
675
+ if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
676
+ printf '%s\n' "$finish_output"
677
+ return 0
678
+ fi
679
+
680
+ printf '%s\n' "$finish_output" >&2
681
+
682
+ if [[ "$AUTO_REVIEW_ON_CONFLICT" -eq 1 ]] && looks_like_conflict_failure "$finish_output"; then
683
+ echo "[codex-agent] Auto-finish hit conflicts. Launching Codex conflict-review pass in sandbox..." >&2
684
+ local review_prompt
685
+ review_prompt="Resolve git conflicts for branch ${branch} against ${finish_base_branch:-dev}, then commit the resolution in this sandbox worktree and exit."
686
+
687
+ (
688
+ cd "$wt"
689
+ set +e
690
+ "$CODEX_BIN" "$review_prompt"
691
+ review_exit="$?"
692
+ set -e
693
+ if [[ "$review_exit" -ne 0 ]]; then
694
+ echo "[codex-agent] Conflict-review Codex pass exited with status ${review_exit}." >&2
695
+ fi
270
696
  )
271
- sys.exit(2)
272
- print(f"{tier_name}\t{model}")
273
- PY
697
+
698
+ if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
699
+ printf '%s\n' "$finish_output"
700
+ return 0
701
+ fi
702
+
703
+ printf '%s\n' "$finish_output" >&2
704
+ fi
705
+
706
+ return 1
274
707
  }
275
708
 
276
- resolved_model=""
277
- resolved_tier=""
278
- resolved_source=""
279
- role_config_path="${OMX_ROLE_MODEL_CONFIG:-${repo_root}/.agents/role-model-tiers.json}"
280
-
281
- if [[ -n "${OMX_ROLE_MODEL_OVERRIDE:-}" ]]; then
282
- resolved_model="$OMX_ROLE_MODEL_OVERRIDE"
283
- resolved_tier="${ROLE:-n/a}"
284
- resolved_source="env-override"
285
- elif [[ -n "$ROLE" ]]; then
286
- if ! lookup_output="$(resolve_role_model "$ROLE" "$role_config_path")"; then
287
- exit 2
288
- fi
289
- resolved_tier="${lookup_output%%$'\t'*}"
290
- resolved_model="${lookup_output##*$'\t'}"
291
- resolved_source="config"
709
+ if ! sync_worktree_with_base "$worktree_path"; then
710
+ exit 1
292
711
  fi
293
712
 
294
- if [[ -n "$resolved_model" ]]; then
295
- echo "[codex-agent] role=${ROLE:-n/a} tier=${resolved_tier} model=${resolved_model} source=${resolved_source}" >&2
296
- CODEX_EXTRA_ARGS+=("-m" "$resolved_model")
713
+ worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
714
+ if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then
715
+ echo "[codex-agent] Could not determine sandbox branch for worktree: $worktree_path" >&2
716
+ exit 1
297
717
  fi
298
718
 
299
- start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
300
- printf '%s\n' "$start_output"
301
-
302
- worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
303
- if [[ -z "$worktree_path" ]]; then
304
- echo "[codex-agent] Could not determine sandbox worktree path from agent-branch-start output." >&2
719
+ if ! ensure_openspec_change_workspace "$worktree_path" "$worktree_branch"; then
305
720
  exit 1
306
721
  fi
307
722
 
308
- if [[ ! -d "$worktree_path" ]]; then
309
- echo "[codex-agent] Reported worktree path does not exist: $worktree_path" >&2
723
+ if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
310
724
  exit 1
311
725
  fi
312
726
 
313
- export_worktree_mem0_env "$worktree_path"
314
-
315
727
  echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
316
728
  cd "$worktree_path"
317
729
  set +e
318
- "$CODEX_BIN" "${CODEX_EXTRA_ARGS[@]}" "$@"
730
+ "$CODEX_BIN" "$@"
319
731
  codex_exit="$?"
320
732
  set -e
321
733
 
322
734
  cd "$repo_root"
323
-
324
735
  final_exit="$codex_exit"
325
- worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
326
- finish_base_branch=""
327
- finish_hint=""
328
- if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
329
- finish_base_branch="$(resolve_finish_base_branch "$worktree_branch")"
330
- finish_hint="$(render_finish_hint "$worktree_branch" "$finish_base_branch")"
331
- fi
332
-
333
- if [[ "$codex_exit" -eq 0 ]]; then
334
- if [[ -x "${repo_root}/scripts/agent-branch-finish.sh" ]]; then
335
- if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
336
- finish_args=(--branch "$worktree_branch")
337
- if [[ -n "$finish_base_branch" ]]; then
338
- finish_args+=(--base "$finish_base_branch")
339
- fi
340
- finish_args+=(--via-pr --wait-for-merge --cleanup)
341
- if [[ -n "$GH_PR_REF" ]]; then
342
- finish_args+=(--pr "$GH_PR_REF")
343
- fi
344
- if [[ -n "$GH_REPO_REF" ]]; then
345
- finish_args+=(--repo "$GH_REPO_REF")
346
- fi
347
-
348
- echo "[codex-agent] Codex finished successfully. Auto-finishing branch via PR merge + cleanup..."
349
- if ! bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}"; then
350
- echo "[codex-agent] Auto-finish failed. Sandbox is kept for manual resolve/retry." >&2
351
- if [[ -n "$finish_hint" ]]; then
352
- echo "[codex-agent] Retry with: ${finish_hint}" >&2
353
- fi
354
- final_exit=1
355
- fi
356
- else
357
- echo "[codex-agent] Could not determine sandbox branch name; skipping auto-finish." >&2
358
- final_exit=1
359
- fi
736
+ auto_finish_completed=0
737
+
738
+ if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
739
+ if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 && "$AUTO_CLEANUP" -eq 1 ]]; then
740
+ echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge -> cleanup."
741
+ elif [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 ]]; then
742
+ echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge (keep branch/worktree)."
743
+ elif [[ "$AUTO_CLEANUP" -eq 1 ]]; then
744
+ echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge -> cleanup."
360
745
  else
361
- echo "[codex-agent] Missing scripts/agent-branch-finish.sh; skipping auto-finish." >&2
362
- final_exit=1
746
+ echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
363
747
  fi
364
- else
365
- if [[ -x "${repo_root}/scripts/agent-branch-finish.sh" && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
366
- echo "[codex-agent] Codex exited with status ${codex_exit}. Attempting best-effort push + PR (no merge wait)..." >&2
367
- fallback_args=(--branch "$worktree_branch" --via-pr --skip-tasks-gate)
368
- if [[ -n "$finish_base_branch" ]]; then
369
- fallback_args+=(--base "$finish_base_branch")
370
- fi
371
- if [[ -n "$GH_PR_REF" ]]; then
372
- fallback_args+=(--pr "$GH_PR_REF")
373
- fi
374
- if [[ -n "$GH_REPO_REF" ]]; then
375
- fallback_args+=(--repo "$GH_REPO_REF")
376
- fi
377
- if bash "${repo_root}/scripts/agent-branch-finish.sh" "${fallback_args[@]}"; then
378
- echo "[codex-agent] Fallback push succeeded: branch pushed + PR created/updated for manual review." >&2
748
+ if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
749
+ if run_finish_flow "$worktree_path" "$worktree_branch"; then
750
+ auto_finish_completed=1
751
+ echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
379
752
  else
380
- echo "[codex-agent] Fallback push failed; sandbox retained. Retry with: ${finish_hint}" >&2
753
+ finish_status="$?"
754
+ if [[ "$finish_status" -eq 2 ]]; then
755
+ echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
756
+ else
757
+ echo "[codex-agent] Auto-finish did not complete; keeping sandbox for manual review: $worktree_path" >&2
758
+ if [[ "$final_exit" -eq 0 ]]; then
759
+ final_exit=1
760
+ fi
761
+ fi
381
762
  fi
382
763
  else
383
- echo "[codex-agent] Skipping auto-finish because Codex exited with status ${codex_exit} and no sandbox branch was detected."
764
+ if [[ "$final_exit" -eq 0 ]]; then
765
+ final_exit=1
766
+ fi
384
767
  fi
385
768
  fi
386
769
 
387
770
  if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
388
- echo "[codex-agent] Session ended (exit=${final_exit}). Running worktree cleanup..."
771
+ echo "[codex-agent] Session ended (exit=${codex_exit}). Running worktree cleanup..."
389
772
  prune_args=()
390
773
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
391
774
  prune_args+=(--base "$BASE_BRANCH")
392
775
  fi
776
+ if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
777
+ prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches)
778
+ fi
393
779
  if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
394
780
  echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
395
781
  fi
@@ -398,13 +784,15 @@ fi
398
784
  if [[ ! -d "$worktree_path" ]]; then
399
785
  echo "[codex-agent] Auto-cleaned sandbox worktree: $worktree_path"
400
786
  else
787
+ worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
401
788
  echo "[codex-agent] Sandbox worktree kept: $worktree_path"
402
789
  if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
403
- if [[ -z "$finish_hint" ]]; then
404
- finish_base_branch="$(resolve_finish_base_branch "$worktree_branch")"
405
- finish_hint="$(render_finish_hint "$worktree_branch" "$finish_base_branch")"
790
+ if [[ "$auto_finish_completed" -eq 1 ]]; then
791
+ echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
792
+ else
793
+ echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --base dev --via-pr --wait-for-merge"
794
+ echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
406
795
  fi
407
- echo "[codex-agent] If finished, merge + clean with: ${finish_hint}"
408
796
  fi
409
797
  fi
410
798