@imdeadpool/guardex 5.0.7 → 5.0.9
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 +50 -4
- package/bin/multiagent-safety.js +989 -14
- package/package.json +1 -1
- package/templates/AGENTS.multiagent-safety.md +11 -2
- package/templates/githooks/pre-commit +20 -6
- package/templates/githooks/pre-push +3 -3
- package/templates/scripts/agent-branch-finish.sh +2 -2
- package/templates/scripts/agent-branch-start.sh +92 -60
- package/templates/scripts/codex-agent.sh +234 -5
package/package.json
CHANGED
|
@@ -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.
|
|
@@ -44,9 +45,17 @@
|
|
|
44
45
|
- Verification commands + results
|
|
45
46
|
- Risks / follow-ups
|
|
46
47
|
|
|
47
|
-
## OpenSpec Plan Workspace (
|
|
48
|
+
## OpenSpec Plan Workspace (required for agent sub-branch changes)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
OMX Codex execution flows must use OpenSpec. `scripts/codex-agent.sh` bootstraps a
|
|
51
|
+
per-branch plan workspace automatically under:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
openspec/plan/<agent-branch-slug>/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with
|
|
58
|
+
`MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
|
|
50
59
|
|
|
51
60
|
```bash
|
|
52
61
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -24,13 +24,13 @@ 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
31
|
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
32
32
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
33
|
-
allow_vscode_protected_raw="
|
|
33
|
+
allow_vscode_protected_raw="true"
|
|
34
34
|
fi
|
|
35
35
|
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
36
36
|
|
|
@@ -55,6 +55,15 @@ for protected_branch in $protected_branches_raw; do
|
|
|
55
55
|
fi
|
|
56
56
|
done
|
|
57
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
|
+
|
|
58
67
|
codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
|
|
59
68
|
if [[ -z "$codex_require_agent_branch_raw" ]]; then
|
|
60
69
|
codex_require_agent_branch_raw="true"
|
|
@@ -124,8 +133,10 @@ MSG
|
|
|
124
133
|
fi
|
|
125
134
|
|
|
126
135
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
127
|
-
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1"
|
|
128
|
-
|
|
136
|
+
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
|
|
137
|
+
if [[ "$allow_vscode_protected_branch_writes" == "1" || "$is_local_only_branch" == "1" ]]; then
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
129
140
|
fi
|
|
130
141
|
|
|
131
142
|
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
|
|
@@ -144,8 +155,11 @@ Use an agent branch first:
|
|
|
144
155
|
After finishing work:
|
|
145
156
|
bash scripts/agent-branch-finish.sh
|
|
146
157
|
|
|
147
|
-
Optional repo
|
|
148
|
-
git config multiagent.allowVscodeProtectedBranchWrites
|
|
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.
|
|
149
163
|
|
|
150
164
|
Temporary bypass (not recommended):
|
|
151
165
|
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
@@ -12,7 +12,7 @@ fi
|
|
|
12
12
|
|
|
13
13
|
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
14
14
|
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
15
|
-
allow_vscode_protected_raw="
|
|
15
|
+
allow_vscode_protected_raw="true"
|
|
16
16
|
fi
|
|
17
17
|
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
18
18
|
|
|
@@ -77,8 +77,8 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
|
|
|
77
77
|
echo "[agent-branch-guard] Push to protected branch blocked."
|
|
78
78
|
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
|
|
79
79
|
echo "[agent-branch-guard] Use an agent branch and merge via PR."
|
|
80
|
-
echo "[agent-branch-guard] Optional VS Code
|
|
81
|
-
echo " git config multiagent.allowVscodeProtectedBranchWrites
|
|
80
|
+
echo "[agent-branch-guard] Optional repo hard-block for VS Code protected-branch push:"
|
|
81
|
+
echo " git config multiagent.allowVscodeProtectedBranchWrites false"
|
|
82
82
|
echo
|
|
83
83
|
echo "Temporary bypass (not recommended):"
|
|
84
84
|
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
|
|
@@ -338,7 +338,7 @@ is_local_branch_delete_error() {
|
|
|
338
338
|
|
|
339
339
|
read_pr_state() {
|
|
340
340
|
local state_line
|
|
341
|
-
state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] |
|
|
341
|
+
state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
|
|
342
342
|
if [[ -z "$state_line" ]]; then
|
|
343
343
|
return 1
|
|
344
344
|
fi
|
|
@@ -346,7 +346,7 @@ read_pr_state() {
|
|
|
346
346
|
local parsed_state=""
|
|
347
347
|
local parsed_merged_at=""
|
|
348
348
|
local parsed_url=""
|
|
349
|
-
IFS=$'\
|
|
349
|
+
IFS=$'\x1f' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
|
|
350
350
|
PR_STATE="$parsed_state"
|
|
351
351
|
PR_MERGED_AT="$parsed_merged_at"
|
|
352
352
|
if [[ -n "$parsed_url" ]]; then
|
|
@@ -5,9 +5,9 @@ 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"
|
|
9
|
+
OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-false}"
|
|
10
|
+
OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
|
|
11
11
|
POSITIONAL_ARGS=()
|
|
12
12
|
|
|
13
13
|
while [[ $# -gt 0 ]]; do
|
|
@@ -25,13 +25,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
25
|
BASE_BRANCH_EXPLICIT=1
|
|
26
26
|
shift 2
|
|
27
27
|
;;
|
|
28
|
-
--in-place)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--allow-in-place)
|
|
33
|
-
ALLOW_IN_PLACE=1
|
|
34
|
-
shift
|
|
28
|
+
--in-place|--allow-in-place)
|
|
29
|
+
echo "[agent-branch-start] In-place branch mode is disabled." >&2
|
|
30
|
+
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
|
|
31
|
+
exit 1
|
|
35
32
|
;;
|
|
36
33
|
--worktree-root)
|
|
37
34
|
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
|
|
@@ -47,7 +44,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
47
44
|
;;
|
|
48
45
|
-*)
|
|
49
46
|
echo "[agent-branch-start] Unknown option: $1" >&2
|
|
50
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
47
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
51
48
|
exit 1
|
|
52
49
|
;;
|
|
53
50
|
*)
|
|
@@ -59,7 +56,7 @@ done
|
|
|
59
56
|
|
|
60
57
|
if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
|
|
61
58
|
echo "[agent-branch-start] Too many positional arguments." >&2
|
|
62
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
59
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
63
60
|
exit 1
|
|
64
61
|
fi
|
|
65
62
|
|
|
@@ -87,6 +84,31 @@ sanitize_slug() {
|
|
|
87
84
|
printf '%s' "$slug"
|
|
88
85
|
}
|
|
89
86
|
|
|
87
|
+
normalize_bool() {
|
|
88
|
+
local raw="${1:-}"
|
|
89
|
+
local fallback="${2:-0}"
|
|
90
|
+
local lowered
|
|
91
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
92
|
+
case "$lowered" in
|
|
93
|
+
1|true|yes|on) printf '1' ;;
|
|
94
|
+
0|false|no|off) printf '0' ;;
|
|
95
|
+
'') printf '%s' "$fallback" ;;
|
|
96
|
+
*) printf '%s' "$fallback" ;;
|
|
97
|
+
esac
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
101
|
+
|
|
102
|
+
resolve_openspec_plan_slug() {
|
|
103
|
+
local branch_name="$1"
|
|
104
|
+
local task_slug="$2"
|
|
105
|
+
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
106
|
+
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
resolve_active_codex_snapshot_name() {
|
|
91
113
|
local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
|
|
92
114
|
if [[ -n "$override" ]]; then
|
|
@@ -119,17 +141,6 @@ has_local_changes() {
|
|
|
119
141
|
return 1
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
has_tracked_changes() {
|
|
123
|
-
local root="$1"
|
|
124
|
-
if ! git -C "$root" diff --quiet; then
|
|
125
|
-
return 0
|
|
126
|
-
fi
|
|
127
|
-
if ! git -C "$root" diff --cached --quiet; then
|
|
128
|
-
return 0
|
|
129
|
-
fi
|
|
130
|
-
return 1
|
|
131
|
-
}
|
|
132
|
-
|
|
133
144
|
resolve_protected_branches() {
|
|
134
145
|
local root="$1"
|
|
135
146
|
local raw
|
|
@@ -182,6 +193,43 @@ hydrate_local_helper_in_worktree() {
|
|
|
182
193
|
echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}"
|
|
183
194
|
}
|
|
184
195
|
|
|
196
|
+
initialize_openspec_plan_workspace() {
|
|
197
|
+
local repo="$1"
|
|
198
|
+
local worktree="$2"
|
|
199
|
+
local plan_slug="$3"
|
|
200
|
+
|
|
201
|
+
hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
|
|
202
|
+
|
|
203
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
local openspec_script="${worktree}/scripts/openspec/init-plan-workspace.sh"
|
|
208
|
+
if [[ ! -f "$openspec_script" ]]; then
|
|
209
|
+
echo "[agent-branch-start] OpenSpec init script is missing in sandbox worktree." >&2
|
|
210
|
+
echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
if [[ ! -x "$openspec_script" ]]; then
|
|
214
|
+
chmod +x "$openspec_script" 2>/dev/null || true
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
local init_output=""
|
|
218
|
+
if ! init_output="$(
|
|
219
|
+
cd "$worktree"
|
|
220
|
+
bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
|
|
221
|
+
)"; then
|
|
222
|
+
printf '%s\n' "$init_output" >&2
|
|
223
|
+
echo "[agent-branch-start] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
|
|
224
|
+
return 1
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
if [[ -n "$init_output" ]]; then
|
|
228
|
+
printf '%s\n' "$init_output"
|
|
229
|
+
fi
|
|
230
|
+
echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}"
|
|
231
|
+
}
|
|
232
|
+
|
|
185
233
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
186
234
|
echo "[agent-branch-start] Not inside a git repository." >&2
|
|
187
235
|
exit 1
|
|
@@ -195,12 +243,15 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
|
195
243
|
fi
|
|
196
244
|
|
|
197
245
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
247
|
+
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
248
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
249
|
+
BASE_BRANCH="$current_branch"
|
|
201
250
|
else
|
|
202
|
-
|
|
203
|
-
if [[ -n "$
|
|
251
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
252
|
+
if [[ -n "$configured_base" ]]; then
|
|
253
|
+
BASE_BRANCH="$configured_base"
|
|
254
|
+
elif [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
|
|
204
255
|
BASE_BRANCH="$current_branch"
|
|
205
256
|
else
|
|
206
257
|
BASE_BRANCH="dev"
|
|
@@ -237,37 +288,10 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
|
237
288
|
branch_suffix=$((branch_suffix + 1))
|
|
238
289
|
done
|
|
239
290
|
|
|
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
291
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
269
292
|
mkdir -p "$worktree_root"
|
|
270
293
|
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
294
|
+
openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
|
|
271
295
|
|
|
272
296
|
if [[ -e "$worktree_path" ]]; then
|
|
273
297
|
echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
|
|
@@ -276,10 +300,11 @@ fi
|
|
|
276
300
|
|
|
277
301
|
auto_transfer_stash_ref=""
|
|
278
302
|
auto_transfer_message=""
|
|
303
|
+
auto_transfer_source_branch=""
|
|
279
304
|
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
280
305
|
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
281
|
-
if [[ "$current_branch"
|
|
282
|
-
if
|
|
306
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
307
|
+
if has_local_changes "$repo_root"; then
|
|
283
308
|
auto_transfer_message="musafety-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
|
|
284
309
|
if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
|
|
285
310
|
auto_transfer_stash_ref="$(
|
|
@@ -287,7 +312,8 @@ if [[ "$current_branch" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_
|
|
|
287
312
|
| awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
|
|
288
313
|
)"
|
|
289
314
|
if [[ -n "$auto_transfer_stash_ref" ]]; then
|
|
290
|
-
|
|
315
|
+
auto_transfer_source_branch="$current_branch"
|
|
316
|
+
echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
|
|
291
317
|
fi
|
|
292
318
|
fi
|
|
293
319
|
fi
|
|
@@ -303,19 +329,25 @@ fi
|
|
|
303
329
|
if [[ -n "$auto_transfer_stash_ref" ]]; then
|
|
304
330
|
if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
|
|
305
331
|
git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
|
|
306
|
-
|
|
332
|
+
transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
|
|
333
|
+
echo "[agent-branch-start] Moved local changes from '${transfer_label}' into '${branch_name}'."
|
|
307
334
|
else
|
|
308
335
|
echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
|
|
309
|
-
|
|
336
|
+
transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
|
|
337
|
+
echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${transfer_label}." >&2
|
|
310
338
|
echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
|
|
311
339
|
exit 1
|
|
312
340
|
fi
|
|
313
341
|
fi
|
|
314
342
|
|
|
315
343
|
hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-agent.sh"
|
|
344
|
+
if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
316
347
|
|
|
317
348
|
echo "[agent-branch-start] Created branch: ${branch_name}"
|
|
318
349
|
echo "[agent-branch-start] Worktree: ${worktree_path}"
|
|
350
|
+
echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
|
|
319
351
|
echo "[agent-branch-start] Next steps:"
|
|
320
352
|
echo " cd \"${worktree_path}\""
|
|
321
353
|
echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
|
|
@@ -10,6 +10,8 @@ AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
|
|
|
10
10
|
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
|
|
11
11
|
AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}"
|
|
12
12
|
AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}"
|
|
13
|
+
OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}"
|
|
14
|
+
OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
|
|
13
15
|
|
|
14
16
|
normalize_bool() {
|
|
15
17
|
local raw="${1:-}"
|
|
@@ -28,6 +30,7 @@ AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
|
|
|
28
30
|
AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"
|
|
29
31
|
AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
|
|
30
32
|
AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
|
|
33
|
+
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
31
34
|
|
|
32
35
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
33
36
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -125,6 +128,146 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
125
128
|
fi
|
|
126
129
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
127
130
|
|
|
131
|
+
sanitize_slug() {
|
|
132
|
+
local raw="$1"
|
|
133
|
+
local fallback="${2:-task}"
|
|
134
|
+
local slug
|
|
135
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
136
|
+
if [[ -z "$slug" ]]; then
|
|
137
|
+
slug="$fallback"
|
|
138
|
+
fi
|
|
139
|
+
printf '%s' "$slug"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resolve_openspec_plan_slug() {
|
|
143
|
+
local branch_name="$1"
|
|
144
|
+
local task_slug
|
|
145
|
+
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
146
|
+
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
147
|
+
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
hydrate_local_helper_in_worktree() {
|
|
154
|
+
local worktree="$1"
|
|
155
|
+
local relative_path="$2"
|
|
156
|
+
local worktree_target="${worktree}/${relative_path}"
|
|
157
|
+
local source_path=""
|
|
158
|
+
|
|
159
|
+
if [[ -e "$worktree_target" ]]; then
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if [[ -f "${repo_root}/${relative_path}" ]]; then
|
|
164
|
+
source_path="${repo_root}/${relative_path}"
|
|
165
|
+
elif [[ -f "${repo_root}/templates/${relative_path}" ]]; then
|
|
166
|
+
source_path="${repo_root}/templates/${relative_path}"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [[ -z "$source_path" ]]; then
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
mkdir -p "$(dirname "$worktree_target")"
|
|
174
|
+
cp "$source_path" "$worktree_target"
|
|
175
|
+
if [[ -x "$source_path" ]]; then
|
|
176
|
+
chmod +x "$worktree_target"
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
resolve_start_base_branch() {
|
|
183
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
184
|
+
printf '%s' "$BASE_BRANCH"
|
|
185
|
+
return 0
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
local configured_base
|
|
189
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
190
|
+
if [[ -n "$configured_base" ]]; then
|
|
191
|
+
printf '%s' "$configured_base"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
local current_branch
|
|
196
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
197
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
|
|
198
|
+
printf '%s' "$current_branch"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
printf 'dev'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
resolve_start_ref() {
|
|
206
|
+
local base_branch="$1"
|
|
207
|
+
git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
|
|
208
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
209
|
+
printf 'origin/%s' "$base_branch"
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
|
|
213
|
+
printf '%s' "$base_branch"
|
|
214
|
+
return 0
|
|
215
|
+
fi
|
|
216
|
+
return 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
restore_repo_branch_if_changed() {
|
|
220
|
+
local expected_branch="$1"
|
|
221
|
+
if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
|
|
222
|
+
return 0
|
|
223
|
+
fi
|
|
224
|
+
local current_branch
|
|
225
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
226
|
+
if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
|
|
227
|
+
return 0
|
|
228
|
+
fi
|
|
229
|
+
git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
start_sandbox_fallback() {
|
|
233
|
+
local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
|
|
234
|
+
local worktree_root worktree_path
|
|
235
|
+
|
|
236
|
+
base_branch="$(resolve_start_base_branch)"
|
|
237
|
+
if ! start_ref="$(resolve_start_ref "$base_branch")"; then
|
|
238
|
+
echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
|
|
239
|
+
return 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
timestamp="$(date +%Y%m%d-%H%M%S)"
|
|
243
|
+
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
244
|
+
agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
|
|
245
|
+
branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
|
|
246
|
+
branch_name="$branch_name_base"
|
|
247
|
+
suffix=2
|
|
248
|
+
while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
|
|
249
|
+
branch_name="${branch_name_base}-${suffix}"
|
|
250
|
+
suffix=$((suffix + 1))
|
|
251
|
+
done
|
|
252
|
+
|
|
253
|
+
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
254
|
+
mkdir -p "$worktree_root"
|
|
255
|
+
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
256
|
+
if [[ -e "$worktree_path" ]]; then
|
|
257
|
+
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
|
|
258
|
+
return 1
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
|
|
262
|
+
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
|
|
263
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
264
|
+
git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
|
|
268
|
+
printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
|
|
269
|
+
}
|
|
270
|
+
|
|
128
271
|
if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
|
|
129
272
|
echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
|
|
130
273
|
exit 1
|
|
@@ -135,12 +278,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
|
135
278
|
start_args+=("$BASE_BRANCH")
|
|
136
279
|
fi
|
|
137
280
|
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
282
|
+
start_output=""
|
|
283
|
+
start_status=0
|
|
284
|
+
set +e
|
|
285
|
+
start_output="$(MUSAFETY_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
|
|
286
|
+
start_status=$?
|
|
287
|
+
set -e
|
|
140
288
|
|
|
141
289
|
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
290
|
+
current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
291
|
+
resolved_repo_root="$(cd "$repo_root" && pwd -P)"
|
|
292
|
+
resolved_worktree_path=""
|
|
293
|
+
if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
|
|
294
|
+
resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
fallback_reason=""
|
|
298
|
+
if [[ "$start_status" -ne 0 ]]; then
|
|
299
|
+
fallback_reason="starter exited with status ${start_status}"
|
|
300
|
+
elif [[ -z "$worktree_path" ]]; then
|
|
301
|
+
fallback_reason="starter did not report worktree path"
|
|
302
|
+
elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
|
|
303
|
+
fallback_reason="starter pointed to active checkout path"
|
|
304
|
+
elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
|
|
305
|
+
fallback_reason="starter switched active checkout branch"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
if [[ -n "$fallback_reason" ]]; then
|
|
309
|
+
if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
|
|
310
|
+
echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
|
|
311
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
if [[ -n "$start_output" ]]; then
|
|
315
|
+
printf '%s\n' "$start_output" >&2
|
|
316
|
+
fi
|
|
317
|
+
echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
|
|
318
|
+
start_output="$(start_sandbox_fallback)"
|
|
319
|
+
printf '%s\n' "$start_output"
|
|
320
|
+
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
|
|
321
|
+
else
|
|
322
|
+
printf '%s\n' "$start_output"
|
|
323
|
+
fi
|
|
324
|
+
|
|
142
325
|
if [[ -z "$worktree_path" ]]; then
|
|
143
|
-
echo "[codex-agent] Could not determine sandbox worktree path from
|
|
326
|
+
echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
|
|
327
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
|
|
144
328
|
exit 1
|
|
145
329
|
fi
|
|
146
330
|
|
|
@@ -222,6 +406,43 @@ sync_worktree_with_base() {
|
|
|
222
406
|
return 0
|
|
223
407
|
}
|
|
224
408
|
|
|
409
|
+
ensure_openspec_plan_workspace() {
|
|
410
|
+
local wt="$1"
|
|
411
|
+
local branch="$2"
|
|
412
|
+
|
|
413
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
|
|
414
|
+
return 0
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh"
|
|
418
|
+
|
|
419
|
+
local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh"
|
|
420
|
+
if [[ ! -f "$openspec_script" ]]; then
|
|
421
|
+
echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2
|
|
422
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
|
|
423
|
+
return 1
|
|
424
|
+
fi
|
|
425
|
+
if [[ ! -x "$openspec_script" ]]; then
|
|
426
|
+
chmod +x "$openspec_script" 2>/dev/null || true
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
local plan_slug
|
|
430
|
+
plan_slug="$(resolve_openspec_plan_slug "$branch")"
|
|
431
|
+
local init_output=""
|
|
432
|
+
if ! init_output="$(
|
|
433
|
+
cd "$wt"
|
|
434
|
+
bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
|
|
435
|
+
)"; then
|
|
436
|
+
printf '%s\n' "$init_output" >&2
|
|
437
|
+
echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
|
|
438
|
+
return 1
|
|
439
|
+
fi
|
|
440
|
+
if [[ -n "$init_output" ]]; then
|
|
441
|
+
printf '%s\n' "$init_output"
|
|
442
|
+
fi
|
|
443
|
+
echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}"
|
|
444
|
+
}
|
|
445
|
+
|
|
225
446
|
worktree_has_changes() {
|
|
226
447
|
local wt="$1"
|
|
227
448
|
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
@@ -438,6 +659,16 @@ if ! sync_worktree_with_base "$worktree_path"; then
|
|
|
438
659
|
exit 1
|
|
439
660
|
fi
|
|
440
661
|
|
|
662
|
+
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
663
|
+
if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then
|
|
664
|
+
echo "[codex-agent] Could not determine sandbox branch for worktree: $worktree_path" >&2
|
|
665
|
+
exit 1
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
|
|
669
|
+
exit 1
|
|
670
|
+
fi
|
|
671
|
+
|
|
441
672
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
442
673
|
cd "$worktree_path"
|
|
443
674
|
set +e
|
|
@@ -449,8 +680,6 @@ cd "$repo_root"
|
|
|
449
680
|
final_exit="$codex_exit"
|
|
450
681
|
auto_finish_completed=0
|
|
451
682
|
|
|
452
|
-
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
453
|
-
|
|
454
683
|
if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
455
684
|
if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 && "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
456
685
|
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge -> cleanup."
|