@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "5.0.7",
3
+ "version": "5.0.9",
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,
@@ -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 (recommended)
48
+ ## OpenSpec Plan Workspace (required for agent sub-branch changes)
48
49
 
49
- When work needs a durable planning phase, scaffold a plan workspace before implementation:
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:-}" || "${TERM_PROGRAM:-}" == "vscode" ]]; then
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="false"
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" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
128
- exit 0
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 override for manual VS Code protected-branch commits:
148
- git config multiagent.allowVscodeProtectedBranchWrites true
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="false"
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 override:"
81
- echo " git config multiagent.allowVscodeProtectedBranchWrites true"
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 // "")] | @tsv' 2>/dev/null || true)"
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=$'\t' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
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
- WORKTREE_MODE=0
30
- shift
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] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
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] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
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
- configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
199
- if [[ -n "$configured_base" ]]; then
200
- BASE_BRANCH="$configured_base"
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
- current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
203
- if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
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" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_BRANCH" "$protected_branches_raw"; then
282
- if has_tracked_changes "$repo_root"; then
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
- echo "[agent-branch-start] Detected local changes on protected base '${BASE_BRANCH}'. Moving them to '${branch_name}'..."
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
- echo "[agent-branch-start] Moved local changes from '${BASE_BRANCH}' into '${branch_name}'."
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
- echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${BASE_BRANCH}." >&2
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
- start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
139
- printf '%s\n' "$start_output"
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 agent-branch-start output." >&2
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."