@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.
- package/README.md +37 -4
- package/bin/multiagent-safety.js +1236 -171
- package/package.json +3 -2
- package/templates/scripts/agent-branch-finish.sh +35 -6
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +93 -15
- package/templates/scripts/agent-worktree-prune.sh +78 -44
- package/templates/scripts/codex-agent.sh +96 -4
- package/templates/scripts/guardex-docker-loader.sh +123 -0
- package/templates/scripts/openspec/init-plan-workspace.sh +42 -0
|
@@ -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="
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
464
|
-
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
continue
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
349
|
+
local entry_common_dir=""
|
|
350
|
+
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
|
|
351
|
+
[[ -n "$entry_common_dir" ]] || continue
|
|
327
352
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
continue
|
|
332
|
-
fi
|
|
353
|
+
if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
|
|
354
|
+
continue
|
|
355
|
+
fi
|
|
333
356
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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}/$
|
|
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="$(
|
|
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
|
-
|
|
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
|
|
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" "$@"
|