@imdeadpool/guardex 5.0.1 → 5.0.3
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/CONTRIBUTING.md +1 -0
- package/README.md +128 -298
- package/bin/multiagent-safety.js +738 -20
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +7 -1
- package/templates/codex/skills/guardex/SKILL.md +2 -0
- package/templates/githooks/pre-commit +22 -0
- package/templates/scripts/agent-branch-finish.sh +216 -33
- package/templates/scripts/agent-branch-start.sh +40 -6
- package/templates/scripts/agent-worktree-prune.sh +57 -14
- package/templates/scripts/codex-agent.sh +318 -2
|
@@ -6,6 +6,28 @@ AGENT_NAME="${MUSAFETY_AGENT_NAME:-agent}"
|
|
|
6
6
|
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
8
|
CODEX_BIN="${MUSAFETY_CODEX_BIN:-codex}"
|
|
9
|
+
AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
|
|
10
|
+
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
|
|
11
|
+
AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}"
|
|
12
|
+
AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}"
|
|
13
|
+
|
|
14
|
+
normalize_bool() {
|
|
15
|
+
local raw="${1:-}"
|
|
16
|
+
local fallback="${2:-0}"
|
|
17
|
+
local lowered
|
|
18
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
19
|
+
case "$lowered" in
|
|
20
|
+
1|true|yes|on) printf '1' ;;
|
|
21
|
+
0|false|no|off) printf '0' ;;
|
|
22
|
+
'') printf '%s' "$fallback" ;;
|
|
23
|
+
*) printf '%s' "$fallback" ;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
|
|
28
|
+
AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"
|
|
29
|
+
AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
|
|
30
|
+
AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
|
|
9
31
|
|
|
10
32
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
11
33
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -30,6 +52,38 @@ while [[ $# -gt 0 ]]; do
|
|
|
30
52
|
CODEX_BIN="${2:-$CODEX_BIN}"
|
|
31
53
|
shift 2
|
|
32
54
|
;;
|
|
55
|
+
--auto-finish)
|
|
56
|
+
AUTO_FINISH=1
|
|
57
|
+
shift
|
|
58
|
+
;;
|
|
59
|
+
--no-auto-finish)
|
|
60
|
+
AUTO_FINISH=0
|
|
61
|
+
shift
|
|
62
|
+
;;
|
|
63
|
+
--auto-review-on-conflict)
|
|
64
|
+
AUTO_REVIEW_ON_CONFLICT=1
|
|
65
|
+
shift
|
|
66
|
+
;;
|
|
67
|
+
--no-auto-review-on-conflict)
|
|
68
|
+
AUTO_REVIEW_ON_CONFLICT=0
|
|
69
|
+
shift
|
|
70
|
+
;;
|
|
71
|
+
--cleanup)
|
|
72
|
+
AUTO_CLEANUP=1
|
|
73
|
+
shift
|
|
74
|
+
;;
|
|
75
|
+
--no-cleanup)
|
|
76
|
+
AUTO_CLEANUP=0
|
|
77
|
+
shift
|
|
78
|
+
;;
|
|
79
|
+
--wait-for-merge)
|
|
80
|
+
AUTO_WAIT_FOR_MERGE=1
|
|
81
|
+
shift
|
|
82
|
+
;;
|
|
83
|
+
--no-wait-for-merge)
|
|
84
|
+
AUTO_WAIT_FOR_MERGE=0
|
|
85
|
+
shift
|
|
86
|
+
;;
|
|
33
87
|
--)
|
|
34
88
|
shift
|
|
35
89
|
break
|
|
@@ -95,6 +149,224 @@ if [[ ! -d "$worktree_path" ]]; then
|
|
|
95
149
|
exit 1
|
|
96
150
|
fi
|
|
97
151
|
|
|
152
|
+
has_origin_remote() {
|
|
153
|
+
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
resolve_worktree_base_branch() {
|
|
157
|
+
local wt="$1"
|
|
158
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
159
|
+
printf '%s' "$BASE_BRANCH"
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
local branch
|
|
164
|
+
branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
165
|
+
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
local stored_base
|
|
170
|
+
stored_base="$(git -C "$repo_root" config --get "branch.${branch}.musafetyBase" || true)"
|
|
171
|
+
if [[ -n "$stored_base" ]]; then
|
|
172
|
+
printf '%s' "$stored_base"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
local configured_base
|
|
177
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
178
|
+
if [[ -n "$configured_base" ]]; then
|
|
179
|
+
printf '%s' "$configured_base"
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sync_worktree_with_base() {
|
|
184
|
+
local wt="$1"
|
|
185
|
+
if ! has_origin_remote; then
|
|
186
|
+
return 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
local base_branch
|
|
190
|
+
base_branch="$(resolve_worktree_base_branch "$wt")"
|
|
191
|
+
if [[ -z "$base_branch" ]]; then
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
|
|
196
|
+
echo "[codex-agent] Warning: could not fetch origin/${base_branch} before task start." >&2
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
local behind_count
|
|
205
|
+
behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
|
|
206
|
+
behind_count="${behind_count:-0}"
|
|
207
|
+
if [[ "$behind_count" -le 0 ]]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
local branch
|
|
212
|
+
branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
213
|
+
echo "[codex-agent] Task sync: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Rebasing before launch..."
|
|
214
|
+
if ! git -C "$wt" rebase "origin/${base_branch}"; then
|
|
215
|
+
echo "[codex-agent] Task sync failed. Resolve and continue in sandbox:" >&2
|
|
216
|
+
echo " git -C \"$wt\" rebase --continue" >&2
|
|
217
|
+
echo " # or abort" >&2
|
|
218
|
+
echo " git -C \"$wt\" rebase --abort" >&2
|
|
219
|
+
return 1
|
|
220
|
+
fi
|
|
221
|
+
echo "[codex-agent] Task sync complete."
|
|
222
|
+
return 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
worktree_has_changes() {
|
|
226
|
+
local wt="$1"
|
|
227
|
+
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
if ! git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
if [[ -n "$(git -C "$wt" ls-files --others --exclude-standard)" ]]; then
|
|
234
|
+
return 0
|
|
235
|
+
fi
|
|
236
|
+
return 1
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
claim_changed_files() {
|
|
240
|
+
local wt="$1"
|
|
241
|
+
local branch="$2"
|
|
242
|
+
local lock_script="${repo_root}/scripts/agent-file-locks.py"
|
|
243
|
+
|
|
244
|
+
if [[ ! -x "$lock_script" ]]; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local changed_raw deleted_raw
|
|
249
|
+
changed_raw="$({
|
|
250
|
+
git -C "$wt" diff --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
251
|
+
git -C "$wt" diff --cached --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
252
|
+
git -C "$wt" ls-files --others --exclude-standard;
|
|
253
|
+
} | sed '/^$/d' | sort -u)"
|
|
254
|
+
|
|
255
|
+
if [[ -n "$changed_raw" ]]; then
|
|
256
|
+
mapfile -t changed_files < <(printf '%s\n' "$changed_raw")
|
|
257
|
+
python3 "$lock_script" claim --branch "$branch" "${changed_files[@]}" >/dev/null 2>&1 || true
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
deleted_raw="$({
|
|
261
|
+
git -C "$wt" diff --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
262
|
+
git -C "$wt" diff --cached --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
263
|
+
} | sed '/^$/d' | sort -u)"
|
|
264
|
+
|
|
265
|
+
if [[ -n "$deleted_raw" ]]; then
|
|
266
|
+
mapfile -t deleted_files < <(printf '%s\n' "$deleted_raw")
|
|
267
|
+
python3 "$lock_script" allow-delete --branch "$branch" "${deleted_files[@]}" >/dev/null 2>&1 || true
|
|
268
|
+
fi
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
auto_commit_worktree_changes() {
|
|
272
|
+
local wt="$1"
|
|
273
|
+
local branch="$2"
|
|
274
|
+
|
|
275
|
+
if ! worktree_has_changes "$wt"; then
|
|
276
|
+
return 0
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
claim_changed_files "$wt" "$branch"
|
|
280
|
+
git -C "$wt" add -A
|
|
281
|
+
|
|
282
|
+
if git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
283
|
+
return 0
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
local default_message="Auto-finish: ${TASK_NAME}"
|
|
287
|
+
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
|
|
288
|
+
|
|
289
|
+
if ! git -C "$wt" commit -m "$commit_message" >/dev/null 2>&1; then
|
|
290
|
+
echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
|
|
291
|
+
return 1
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
looks_like_conflict_failure() {
|
|
299
|
+
local output="$1"
|
|
300
|
+
if grep -qiE 'preflight conflict detected|merge conflict detected|auto-sync failed while rebasing|rebase --continue|rebase --abort' <<< "$output"; then
|
|
301
|
+
return 0
|
|
302
|
+
fi
|
|
303
|
+
return 1
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
run_finish_flow() {
|
|
307
|
+
local wt="$1"
|
|
308
|
+
local branch="$2"
|
|
309
|
+
local finish_output=""
|
|
310
|
+
local -a finish_args
|
|
311
|
+
|
|
312
|
+
finish_args=(--branch "$branch")
|
|
313
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
314
|
+
finish_args+=(--base "$BASE_BRANCH")
|
|
315
|
+
fi
|
|
316
|
+
if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
317
|
+
finish_args+=(--cleanup)
|
|
318
|
+
fi
|
|
319
|
+
if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 ]]; then
|
|
320
|
+
finish_args+=(--wait-for-merge)
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if has_origin_remote; then
|
|
324
|
+
if command -v gh >/dev/null 2>&1 || command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1; then
|
|
325
|
+
finish_args+=(--via-pr)
|
|
326
|
+
fi
|
|
327
|
+
else
|
|
328
|
+
echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
|
|
329
|
+
return 2
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
|
|
333
|
+
printf '%s\n' "$finish_output"
|
|
334
|
+
return 0
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
printf '%s\n' "$finish_output" >&2
|
|
338
|
+
|
|
339
|
+
if [[ "$AUTO_REVIEW_ON_CONFLICT" -eq 1 ]] && looks_like_conflict_failure "$finish_output"; then
|
|
340
|
+
echo "[codex-agent] Auto-finish hit conflicts. Launching Codex conflict-review pass in sandbox..." >&2
|
|
341
|
+
local review_prompt
|
|
342
|
+
review_prompt="Resolve git conflicts for branch ${branch} against ${BASE_BRANCH:-base branch}, then commit the resolution in this sandbox worktree and exit."
|
|
343
|
+
|
|
344
|
+
(
|
|
345
|
+
cd "$wt"
|
|
346
|
+
set +e
|
|
347
|
+
"$CODEX_BIN" "$review_prompt"
|
|
348
|
+
review_exit="$?"
|
|
349
|
+
set -e
|
|
350
|
+
if [[ "$review_exit" -ne 0 ]]; then
|
|
351
|
+
echo "[codex-agent] Conflict-review Codex pass exited with status ${review_exit}." >&2
|
|
352
|
+
fi
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
|
|
356
|
+
printf '%s\n' "$finish_output"
|
|
357
|
+
return 0
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
printf '%s\n' "$finish_output" >&2
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
return 1
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if ! sync_worktree_with_base "$worktree_path"; then
|
|
367
|
+
exit 1
|
|
368
|
+
fi
|
|
369
|
+
|
|
98
370
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
99
371
|
cd "$worktree_path"
|
|
100
372
|
set +e
|
|
@@ -103,6 +375,42 @@ codex_exit="$?"
|
|
|
103
375
|
set -e
|
|
104
376
|
|
|
105
377
|
cd "$repo_root"
|
|
378
|
+
final_exit="$codex_exit"
|
|
379
|
+
auto_finish_completed=0
|
|
380
|
+
|
|
381
|
+
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
382
|
+
|
|
383
|
+
if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
384
|
+
if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 && "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
385
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge -> cleanup."
|
|
386
|
+
elif [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 ]]; then
|
|
387
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge (keep branch/worktree)."
|
|
388
|
+
elif [[ "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
389
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge -> cleanup."
|
|
390
|
+
else
|
|
391
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
|
|
392
|
+
fi
|
|
393
|
+
if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
|
|
394
|
+
if run_finish_flow "$worktree_path" "$worktree_branch"; then
|
|
395
|
+
auto_finish_completed=1
|
|
396
|
+
echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
|
|
397
|
+
else
|
|
398
|
+
finish_status="$?"
|
|
399
|
+
if [[ "$finish_status" -eq 2 ]]; then
|
|
400
|
+
echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
|
|
401
|
+
else
|
|
402
|
+
echo "[codex-agent] Auto-finish did not complete; keeping sandbox for manual review: $worktree_path" >&2
|
|
403
|
+
if [[ "$final_exit" -eq 0 ]]; then
|
|
404
|
+
final_exit=1
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
408
|
+
else
|
|
409
|
+
if [[ "$final_exit" -eq 0 ]]; then
|
|
410
|
+
final_exit=1
|
|
411
|
+
fi
|
|
412
|
+
fi
|
|
413
|
+
fi
|
|
106
414
|
|
|
107
415
|
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
108
416
|
echo "[codex-agent] Session ended (exit=${codex_exit}). Running worktree cleanup..."
|
|
@@ -110,6 +418,9 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
|
110
418
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
111
419
|
prune_args+=(--base "$BASE_BRANCH")
|
|
112
420
|
fi
|
|
421
|
+
if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
|
|
422
|
+
prune_args+=(--delete-branches --delete-remote-branches)
|
|
423
|
+
fi
|
|
113
424
|
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
114
425
|
echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
|
|
115
426
|
fi
|
|
@@ -121,8 +432,13 @@ else
|
|
|
121
432
|
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
122
433
|
echo "[codex-agent] Sandbox worktree kept: $worktree_path"
|
|
123
434
|
if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
124
|
-
|
|
435
|
+
if [[ "$auto_finish_completed" -eq 1 ]]; then
|
|
436
|
+
echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
437
|
+
else
|
|
438
|
+
echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --via-pr"
|
|
439
|
+
echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
440
|
+
fi
|
|
125
441
|
fi
|
|
126
442
|
fi
|
|
127
443
|
|
|
128
|
-
exit "$
|
|
444
|
+
exit "$final_exit"
|