@imdeadpool/guardex 5.0.5 → 5.0.8
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 +29 -4
- package/bin/multiagent-safety.js +496 -12
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +3 -1
- package/templates/githooks/pre-commit +32 -2
- package/templates/githooks/pre-push +18 -3
- package/templates/scripts/agent-branch-finish.sh +1 -1
- package/templates/scripts/agent-branch-start.sh +6 -39
- package/templates/scripts/agent-worktree-prune.sh +103 -1
- package/templates/scripts/codex-agent.sh +219 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
4
4
|
"description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"agent:safety:setup": "gx setup",
|
|
29
29
|
"agent:safety:scan": "gx scan",
|
|
30
30
|
"agent:safety:fix": "gx fix",
|
|
31
|
-
"agent:safety:doctor": "gx doctor"
|
|
31
|
+
"agent:safety:doctor": "gx doctor",
|
|
32
|
+
"agent:review:watch": "bash ./scripts/review-bot-watch.sh"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=18"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
|
|
11
11
|
- If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
|
|
12
12
|
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
|
|
13
|
+
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
|
|
13
14
|
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
|
|
14
15
|
- Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
|
|
15
16
|
- Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
|
|
@@ -17,7 +18,8 @@
|
|
|
17
18
|
- If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge` and keep the branch open until checks/review pass.
|
|
18
19
|
- If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --via-pr` until merged.
|
|
19
20
|
- Completion is not valid until these are true: commit exists on the agent branch, branch is pushed to `origin`, and PR/merge status is produced by `agent-branch-finish.sh` or `codex-agent`.
|
|
20
|
-
-
|
|
21
|
+
- For every new task, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch; otherwise create a fresh one from the current local base snapshot with `scripts/agent-branch-start.sh`.
|
|
22
|
+
- Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
21
23
|
- If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
|
|
22
24
|
|
|
23
25
|
1. Explicit ownership before edits
|
|
@@ -24,10 +24,23 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
|
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
26
|
is_vscode_git_context=0
|
|
27
|
-
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}"
|
|
27
|
+
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
|
|
28
28
|
is_vscode_git_context=1
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
+
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
32
|
+
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
33
|
+
allow_vscode_protected_raw="true"
|
|
34
|
+
fi
|
|
35
|
+
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
36
|
+
|
|
37
|
+
allow_vscode_protected_branch_writes=0
|
|
38
|
+
case "$allow_vscode_protected" in
|
|
39
|
+
1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
|
|
40
|
+
0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
|
|
41
|
+
*) allow_vscode_protected_branch_writes=0 ;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
31
44
|
protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
32
45
|
if [[ -z "$protected_branches_raw" ]]; then
|
|
33
46
|
protected_branches_raw="dev main master"
|
|
@@ -42,6 +55,15 @@ for protected_branch in $protected_branches_raw; do
|
|
|
42
55
|
fi
|
|
43
56
|
done
|
|
44
57
|
|
|
58
|
+
is_local_only_branch=0
|
|
59
|
+
if [[ "$is_protected_branch" == "1" ]]; then
|
|
60
|
+
upstream_ref="$(git for-each-ref --format='%(upstream:short)' "refs/heads/${branch}" | head -n 1)"
|
|
61
|
+
remote_branch_ref="$(git for-each-ref --format='%(refname:short)' "refs/remotes/*/${branch}" | head -n 1)"
|
|
62
|
+
if [[ -z "$upstream_ref" && -z "$remote_branch_ref" ]]; then
|
|
63
|
+
is_local_only_branch=1
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
45
67
|
codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
|
|
46
68
|
if [[ -z "$codex_require_agent_branch_raw" ]]; then
|
|
47
69
|
codex_require_agent_branch_raw="true"
|
|
@@ -112,7 +134,9 @@ fi
|
|
|
112
134
|
|
|
113
135
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
114
136
|
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
|
|
115
|
-
|
|
137
|
+
if [[ "$allow_vscode_protected_branch_writes" == "1" || "$is_local_only_branch" == "1" ]]; then
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
116
140
|
fi
|
|
117
141
|
|
|
118
142
|
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
|
|
@@ -131,6 +155,12 @@ Use an agent branch first:
|
|
|
131
155
|
After finishing work:
|
|
132
156
|
bash scripts/agent-branch-finish.sh
|
|
133
157
|
|
|
158
|
+
Optional repo hard-block for VS Code protected-branch commits:
|
|
159
|
+
git config multiagent.allowVscodeProtectedBranchWrites false
|
|
160
|
+
|
|
161
|
+
VS Code Source Control commits on protected local-only branches
|
|
162
|
+
(no upstream and no remote branch) are allowed automatically.
|
|
163
|
+
|
|
134
164
|
Temporary bypass (not recommended):
|
|
135
165
|
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
136
166
|
MSG
|
|
@@ -10,6 +10,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
|
|
|
10
10
|
is_vscode_git_context=1
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
+
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
14
|
+
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
15
|
+
allow_vscode_protected_raw="true"
|
|
16
|
+
fi
|
|
17
|
+
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
18
|
+
|
|
19
|
+
allow_vscode_protected_branch_writes=0
|
|
20
|
+
case "$allow_vscode_protected" in
|
|
21
|
+
1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
|
|
22
|
+
0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
|
|
23
|
+
*) allow_vscode_protected_branch_writes=0 ;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
13
26
|
is_codex_session=0
|
|
14
27
|
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
|
|
15
28
|
is_codex_session=1
|
|
@@ -56,14 +69,16 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
|
|
|
56
69
|
exit 1
|
|
57
70
|
fi
|
|
58
71
|
|
|
59
|
-
if [[ "$is_vscode_git_context" == "1" ]]; then
|
|
72
|
+
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
|
|
60
73
|
exit 0
|
|
61
74
|
fi
|
|
62
75
|
|
|
63
76
|
{
|
|
64
|
-
echo "[agent-branch-guard] Push to protected branch blocked
|
|
77
|
+
echo "[agent-branch-guard] Push to protected branch blocked."
|
|
65
78
|
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
|
|
66
|
-
echo "[agent-branch-guard] Use
|
|
79
|
+
echo "[agent-branch-guard] Use an agent branch and merge via PR."
|
|
80
|
+
echo "[agent-branch-guard] Optional repo hard-block for VS Code protected-branch push:"
|
|
81
|
+
echo " git config multiagent.allowVscodeProtectedBranchWrites false"
|
|
67
82
|
echo
|
|
68
83
|
echo "Temporary bypass (not recommended):"
|
|
69
84
|
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
|
|
@@ -545,7 +545,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
|
545
545
|
fi
|
|
546
546
|
|
|
547
547
|
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
548
|
-
prune_args=(--base "$BASE_BRANCH" --delete-branches)
|
|
548
|
+
prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
|
|
549
549
|
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
550
550
|
prune_args+=(--delete-remote-branches)
|
|
551
551
|
fi
|
|
@@ -5,8 +5,6 @@ TASK_NAME="task"
|
|
|
5
5
|
AGENT_NAME="agent"
|
|
6
6
|
BASE_BRANCH=""
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
|
-
WORKTREE_MODE=1
|
|
9
|
-
ALLOW_IN_PLACE=0
|
|
10
8
|
WORKTREE_ROOT_REL=".omx/agent-worktrees"
|
|
11
9
|
POSITIONAL_ARGS=()
|
|
12
10
|
|
|
@@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
23
|
BASE_BRANCH_EXPLICIT=1
|
|
26
24
|
shift 2
|
|
27
25
|
;;
|
|
28
|
-
--in-place)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--allow-in-place)
|
|
33
|
-
ALLOW_IN_PLACE=1
|
|
34
|
-
shift
|
|
26
|
+
--in-place|--allow-in-place)
|
|
27
|
+
echo "[agent-branch-start] In-place branch mode is disabled." >&2
|
|
28
|
+
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
|
|
29
|
+
exit 1
|
|
35
30
|
;;
|
|
36
31
|
--worktree-root)
|
|
37
32
|
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
|
|
@@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
47
42
|
;;
|
|
48
43
|
-*)
|
|
49
44
|
echo "[agent-branch-start] Unknown option: $1" >&2
|
|
50
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
45
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
51
46
|
exit 1
|
|
52
47
|
;;
|
|
53
48
|
*)
|
|
@@ -59,7 +54,7 @@ done
|
|
|
59
54
|
|
|
60
55
|
if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
|
|
61
56
|
echo "[agent-branch-start] Too many positional arguments." >&2
|
|
62
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
57
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
63
58
|
exit 1
|
|
64
59
|
fi
|
|
65
60
|
|
|
@@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
|
237
232
|
branch_suffix=$((branch_suffix + 1))
|
|
238
233
|
done
|
|
239
234
|
|
|
240
|
-
if [[ "$WORKTREE_MODE" -eq 0 ]]; then
|
|
241
|
-
if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
|
|
242
|
-
echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
|
|
243
|
-
echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
|
|
244
|
-
exit 1
|
|
245
|
-
fi
|
|
246
|
-
|
|
247
|
-
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
248
|
-
echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
|
|
249
|
-
exit 1
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
|
-
current_branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
253
|
-
if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
|
|
254
|
-
git checkout "$BASE_BRANCH"
|
|
255
|
-
fi
|
|
256
|
-
|
|
257
|
-
if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
258
|
-
git pull --ff-only origin "$BASE_BRANCH"
|
|
259
|
-
fi
|
|
260
|
-
|
|
261
|
-
git checkout -b "$branch_name"
|
|
262
|
-
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
263
|
-
echo "[agent-branch-start] Created in-place branch: ${branch_name}"
|
|
264
|
-
echo "$branch_name"
|
|
265
|
-
exit 0
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
235
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
269
236
|
mkdir -p "$worktree_root"
|
|
270
237
|
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
@@ -7,6 +7,7 @@ DRY_RUN=0
|
|
|
7
7
|
FORCE_DIRTY=0
|
|
8
8
|
DELETE_BRANCHES=0
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
|
+
ONLY_DIRTY_WORKTREES=0
|
|
10
11
|
TARGET_BRANCH=""
|
|
11
12
|
|
|
12
13
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
@@ -36,13 +37,17 @@ while [[ $# -gt 0 ]]; do
|
|
|
36
37
|
DELETE_REMOTE_BRANCHES=1
|
|
37
38
|
shift
|
|
38
39
|
;;
|
|
40
|
+
--only-dirty-worktrees)
|
|
41
|
+
ONLY_DIRTY_WORKTREES=1
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
39
44
|
--branch)
|
|
40
45
|
TARGET_BRANCH="${2:-}"
|
|
41
46
|
shift 2
|
|
42
47
|
;;
|
|
43
48
|
*)
|
|
44
49
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
45
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
|
|
50
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
|
|
46
51
|
exit 1
|
|
47
52
|
;;
|
|
48
53
|
esac
|
|
@@ -56,6 +61,11 @@ fi
|
|
|
56
61
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
57
62
|
current_pwd="$(pwd -P)"
|
|
58
63
|
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
64
|
+
repo_common_dir="$(
|
|
65
|
+
git -C "$repo_root" rev-parse --git-common-dir \
|
|
66
|
+
| awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
|
|
67
|
+
)"
|
|
68
|
+
repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
|
|
59
69
|
|
|
60
70
|
resolve_base_branch() {
|
|
61
71
|
local configured=""
|
|
@@ -127,11 +137,98 @@ is_clean_worktree() {
|
|
|
127
137
|
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
resolve_worktree_common_dir() {
|
|
141
|
+
local wt="$1"
|
|
142
|
+
local common_dir=""
|
|
143
|
+
common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null || true)"
|
|
144
|
+
if [[ -z "$common_dir" ]]; then
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
if [[ "$common_dir" == /* ]]; then
|
|
148
|
+
common_dir="$(cd "$common_dir" 2>/dev/null && pwd -P || true)"
|
|
149
|
+
else
|
|
150
|
+
common_dir="$(cd "$wt/$common_dir" 2>/dev/null && pwd -P || true)"
|
|
151
|
+
fi
|
|
152
|
+
if [[ -z "$common_dir" ]]; then
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
printf '%s' "$common_dir"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
select_unique_worktree_path() {
|
|
159
|
+
local root="$1"
|
|
160
|
+
local name="$2"
|
|
161
|
+
local candidate="${root}/${name}"
|
|
162
|
+
local suffix=2
|
|
163
|
+
while [[ -e "$candidate" ]]; do
|
|
164
|
+
candidate="${root}/${name}-${suffix}"
|
|
165
|
+
suffix=$((suffix + 1))
|
|
166
|
+
done
|
|
167
|
+
printf '%s' "$candidate"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
relocated_foreign=0
|
|
171
|
+
skipped_foreign=0
|
|
172
|
+
|
|
173
|
+
relocate_foreign_worktree_entries() {
|
|
174
|
+
[[ -d "$worktree_root" ]] || return 0
|
|
175
|
+
|
|
176
|
+
local entry=""
|
|
177
|
+
for entry in "${worktree_root}"/*; do
|
|
178
|
+
[[ -d "$entry" ]] || continue
|
|
179
|
+
if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
180
|
+
continue
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
local entry_common_dir=""
|
|
184
|
+
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
|
|
185
|
+
[[ -n "$entry_common_dir" ]] || continue
|
|
186
|
+
|
|
187
|
+
if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
|
|
192
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
193
|
+
echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
|
|
194
|
+
continue
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
local owner_repo_root
|
|
198
|
+
owner_repo_root="$(dirname "$entry_common_dir")"
|
|
199
|
+
local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
|
|
200
|
+
local target_path
|
|
201
|
+
target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
|
|
202
|
+
|
|
203
|
+
if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
|
|
204
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
205
|
+
echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
|
|
206
|
+
continue
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
|
|
210
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
211
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
212
|
+
continue
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
mkdir -p "$owner_worktree_root"
|
|
216
|
+
if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
|
|
217
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
218
|
+
else
|
|
219
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
220
|
+
echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
|
|
221
|
+
fi
|
|
222
|
+
done
|
|
223
|
+
}
|
|
224
|
+
|
|
130
225
|
removed_worktrees=0
|
|
131
226
|
removed_branches=0
|
|
132
227
|
skipped_active=0
|
|
133
228
|
skipped_dirty=0
|
|
134
229
|
|
|
230
|
+
relocate_foreign_worktree_entries
|
|
231
|
+
|
|
135
232
|
process_entry() {
|
|
136
233
|
local wt="$1"
|
|
137
234
|
local branch_ref="$2"
|
|
@@ -165,6 +262,8 @@ process_entry() {
|
|
|
165
262
|
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
166
263
|
remove_reason="merged-agent-branch"
|
|
167
264
|
fi
|
|
265
|
+
elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
|
|
266
|
+
remove_reason="clean-agent-worktree"
|
|
168
267
|
fi
|
|
169
268
|
elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
|
|
170
269
|
remove_reason="temporary-worktree"
|
|
@@ -258,6 +357,9 @@ fi
|
|
|
258
357
|
run_cmd git -C "$repo_root" worktree prune
|
|
259
358
|
|
|
260
359
|
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
|
|
360
|
+
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
|
|
361
|
+
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
|
|
362
|
+
fi
|
|
261
363
|
if [[ "$skipped_active" -gt 0 ]]; then
|
|
262
364
|
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
|
|
263
365
|
fi
|
|
@@ -125,6 +125,106 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
125
125
|
fi
|
|
126
126
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
127
127
|
|
|
128
|
+
sanitize_slug() {
|
|
129
|
+
local raw="$1"
|
|
130
|
+
local fallback="${2:-task}"
|
|
131
|
+
local slug
|
|
132
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
133
|
+
if [[ -z "$slug" ]]; then
|
|
134
|
+
slug="$fallback"
|
|
135
|
+
fi
|
|
136
|
+
printf '%s' "$slug"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resolve_start_base_branch() {
|
|
140
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
141
|
+
printf '%s' "$BASE_BRANCH"
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
local configured_base
|
|
146
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
147
|
+
if [[ -n "$configured_base" ]]; then
|
|
148
|
+
printf '%s' "$configured_base"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
local current_branch
|
|
153
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
154
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
|
|
155
|
+
printf '%s' "$current_branch"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
printf 'dev'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resolve_start_ref() {
|
|
163
|
+
local base_branch="$1"
|
|
164
|
+
git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
|
|
165
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
166
|
+
printf 'origin/%s' "$base_branch"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
|
|
170
|
+
printf '%s' "$base_branch"
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
return 1
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
restore_repo_branch_if_changed() {
|
|
177
|
+
local expected_branch="$1"
|
|
178
|
+
if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
local current_branch
|
|
182
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
183
|
+
if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
start_sandbox_fallback() {
|
|
190
|
+
local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
|
|
191
|
+
local worktree_root worktree_path
|
|
192
|
+
|
|
193
|
+
base_branch="$(resolve_start_base_branch)"
|
|
194
|
+
if ! start_ref="$(resolve_start_ref "$base_branch")"; then
|
|
195
|
+
echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
timestamp="$(date +%Y%m%d-%H%M%S)"
|
|
200
|
+
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
201
|
+
agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
|
|
202
|
+
branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
|
|
203
|
+
branch_name="$branch_name_base"
|
|
204
|
+
suffix=2
|
|
205
|
+
while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
206
|
+
branch_name="${branch_name_base}-${suffix}"
|
|
207
|
+
suffix=$((suffix + 1))
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
211
|
+
mkdir -p "$worktree_root"
|
|
212
|
+
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
213
|
+
if [[ -e "$worktree_path" ]]; then
|
|
214
|
+
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
|
|
219
|
+
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
|
|
220
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
221
|
+
git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
|
|
225
|
+
printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
|
|
226
|
+
}
|
|
227
|
+
|
|
128
228
|
if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
|
|
129
229
|
echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
|
|
130
230
|
exit 1
|
|
@@ -135,12 +235,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
|
135
235
|
start_args+=("$BASE_BRANCH")
|
|
136
236
|
fi
|
|
137
237
|
|
|
138
|
-
|
|
139
|
-
|
|
238
|
+
initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
239
|
+
start_output=""
|
|
240
|
+
start_status=0
|
|
241
|
+
set +e
|
|
242
|
+
start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
|
|
243
|
+
start_status=$?
|
|
244
|
+
set -e
|
|
140
245
|
|
|
141
246
|
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
247
|
+
current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
248
|
+
resolved_repo_root="$(cd "$repo_root" && pwd -P)"
|
|
249
|
+
resolved_worktree_path=""
|
|
250
|
+
if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
|
|
251
|
+
resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
fallback_reason=""
|
|
255
|
+
if [[ "$start_status" -ne 0 ]]; then
|
|
256
|
+
fallback_reason="starter exited with status ${start_status}"
|
|
257
|
+
elif [[ -z "$worktree_path" ]]; then
|
|
258
|
+
fallback_reason="starter did not report worktree path"
|
|
259
|
+
elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
|
|
260
|
+
fallback_reason="starter pointed to active checkout path"
|
|
261
|
+
elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
|
|
262
|
+
fallback_reason="starter switched active checkout branch"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if [[ -n "$fallback_reason" ]]; then
|
|
266
|
+
if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
|
|
267
|
+
echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
|
|
268
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
269
|
+
exit 1
|
|
270
|
+
fi
|
|
271
|
+
if [[ -n "$start_output" ]]; then
|
|
272
|
+
printf '%s\n' "$start_output" >&2
|
|
273
|
+
fi
|
|
274
|
+
echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
|
|
275
|
+
start_output="$(start_sandbox_fallback)"
|
|
276
|
+
printf '%s\n' "$start_output"
|
|
277
|
+
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
278
|
+
else
|
|
279
|
+
printf '%s\n' "$start_output"
|
|
280
|
+
fi
|
|
281
|
+
|
|
142
282
|
if [[ -z "$worktree_path" ]]; then
|
|
143
|
-
echo "[codex-agent] Could not determine sandbox worktree path from
|
|
283
|
+
echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
|
|
284
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
144
285
|
exit 1
|
|
145
286
|
fi
|
|
146
287
|
|
|
@@ -285,13 +426,84 @@ auto_commit_worktree_changes() {
|
|
|
285
426
|
|
|
286
427
|
local default_message="Auto-finish: ${TASK_NAME}"
|
|
287
428
|
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
|
|
429
|
+
local commit_output=""
|
|
430
|
+
|
|
431
|
+
if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
|
|
432
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
|
|
433
|
+
return 0
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
if auto_sync_for_commit_retry "$wt" "$branch"; then
|
|
437
|
+
claim_changed_files "$wt" "$branch"
|
|
438
|
+
git -C "$wt" add -A
|
|
439
|
+
if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
|
|
440
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}' after sync retry."
|
|
441
|
+
return 0
|
|
442
|
+
fi
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
|
|
446
|
+
if [[ -n "$commit_output" ]]; then
|
|
447
|
+
printf '%s\n' "$commit_output" >&2
|
|
448
|
+
fi
|
|
449
|
+
return 1
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
auto_sync_for_commit_retry() {
|
|
453
|
+
local wt="$1"
|
|
454
|
+
local branch="$2"
|
|
455
|
+
|
|
456
|
+
if ! has_origin_remote; then
|
|
457
|
+
return 1
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
local base_branch
|
|
461
|
+
base_branch="$(resolve_worktree_base_branch "$wt")"
|
|
462
|
+
if [[ -z "$base_branch" ]]; then
|
|
463
|
+
return 1
|
|
464
|
+
fi
|
|
288
465
|
|
|
289
|
-
if ! git -C "$wt"
|
|
290
|
-
|
|
466
|
+
if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
|
|
467
|
+
return 1
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
291
471
|
return 1
|
|
292
472
|
fi
|
|
293
473
|
|
|
294
|
-
|
|
474
|
+
local behind_count
|
|
475
|
+
behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
|
|
476
|
+
behind_count="${behind_count:-0}"
|
|
477
|
+
if [[ "$behind_count" -le 0 ]]; then
|
|
478
|
+
return 1
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
echo "[codex-agent] Auto-commit retry: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Syncing and retrying..."
|
|
482
|
+
|
|
483
|
+
local stash_ref=""
|
|
484
|
+
local stash_output=""
|
|
485
|
+
if worktree_has_changes "$wt"; then
|
|
486
|
+
if ! stash_output="$(git -C "$wt" stash push --include-untracked -m "codex-agent-autocommit-sync-${branch}-$(date +%s)" 2>&1)"; then
|
|
487
|
+
return 1
|
|
488
|
+
fi
|
|
489
|
+
stash_ref="$(printf '%s\n' "$stash_output" | grep -o 'stash@{[0-9]\+}' | head -n 1 || true)"
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
if ! git -C "$wt" rebase "origin/${base_branch}" >/dev/null 2>&1; then
|
|
493
|
+
git -C "$wt" rebase --abort >/dev/null 2>&1 || true
|
|
494
|
+
if [[ -n "$stash_ref" ]]; then
|
|
495
|
+
git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1 || true
|
|
496
|
+
fi
|
|
497
|
+
return 1
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
if [[ -n "$stash_ref" ]]; then
|
|
501
|
+
if ! git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1; then
|
|
502
|
+
echo "[codex-agent] Auto-commit retry could not re-apply local changes after sync. Manual resolution required in: $wt" >&2
|
|
503
|
+
return 1
|
|
504
|
+
fi
|
|
505
|
+
fi
|
|
506
|
+
|
|
295
507
|
return 0
|
|
296
508
|
}
|
|
297
509
|
|
|
@@ -419,7 +631,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
|
419
631
|
prune_args+=(--base "$BASE_BRANCH")
|
|
420
632
|
fi
|
|
421
633
|
if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
|
|
422
|
-
prune_args+=(--delete-branches --delete-remote-branches)
|
|
634
|
+
prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches)
|
|
423
635
|
fi
|
|
424
636
|
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
425
637
|
echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
|