@imdeadpool/guardex 5.0.8 → 5.0.11

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.8",
3
+ "version": "5.0.11",
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,
@@ -29,7 +29,8 @@
29
29
  "agent:safety:scan": "gx scan",
30
30
  "agent:safety:fix": "gx fix",
31
31
  "agent:safety:doctor": "gx doctor",
32
- "agent:review:watch": "bash ./scripts/review-bot-watch.sh"
32
+ "agent:review:watch": "bash ./scripts/review-bot-watch.sh",
33
+ "agent:finish": "gx finish --all"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=18"
@@ -1,5 +1,5 @@
1
1
  <!-- multiagent-safety:START -->
2
- ## Multi-Agent Execution Contract (multiagent-safety)
2
+ ## Multi-Agent Execution Contract (GX)
3
3
 
4
4
  0. Session plan comment + read gate (required)
5
5
 
@@ -13,6 +13,7 @@
13
13
  - In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
14
14
  - Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
15
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).
16
+ - OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR for those changes (via `codex-agent` or `agent-branch-finish`).
16
17
  - Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
17
18
  - Use `--no-cleanup` only when you explicitly need to keep a merged sandbox for audit/debug follow-up.
18
19
  - 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.
@@ -45,9 +46,17 @@
45
46
  - Verification commands + results
46
47
  - Risks / follow-ups
47
48
 
48
- ## OpenSpec Plan Workspace (recommended)
49
+ ## OpenSpec Plan Workspace (required for agent sub-branch changes)
49
50
 
50
- When work needs a durable planning phase, scaffold a plan workspace before implementation:
51
+ OMX Codex execution flows must use OpenSpec. `scripts/codex-agent.sh` bootstraps a
52
+ per-branch plan workspace automatically under:
53
+
54
+ ```text
55
+ openspec/plan/<agent-branch-slug>/
56
+ ```
57
+
58
+ For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with
59
+ `MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
51
60
 
52
61
  ```bash
53
62
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
@@ -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
@@ -6,6 +6,8 @@ AGENT_NAME="agent"
6
6
  BASE_BRANCH=""
7
7
  BASE_BRANCH_EXPLICIT=0
8
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:-}"
9
11
  POSITIONAL_ARGS=()
10
12
 
11
13
  while [[ $# -gt 0 ]]; do
@@ -82,6 +84,31 @@ sanitize_slug() {
82
84
  printf '%s' "$slug"
83
85
  }
84
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
+
85
112
  resolve_active_codex_snapshot_name() {
86
113
  local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
87
114
  if [[ -n "$override" ]]; then
@@ -114,17 +141,6 @@ has_local_changes() {
114
141
  return 1
115
142
  }
116
143
 
117
- has_tracked_changes() {
118
- local root="$1"
119
- if ! git -C "$root" diff --quiet; then
120
- return 0
121
- fi
122
- if ! git -C "$root" diff --cached --quiet; then
123
- return 0
124
- fi
125
- return 1
126
- }
127
-
128
144
  resolve_protected_branches() {
129
145
  local root="$1"
130
146
  local raw
@@ -177,6 +193,63 @@ hydrate_local_helper_in_worktree() {
177
193
  echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}"
178
194
  }
179
195
 
196
+ hydrate_dependency_dir_symlink_in_worktree() {
197
+ local repo="$1"
198
+ local worktree="$2"
199
+ local relative_path="$3"
200
+ local source_path="${repo}/${relative_path}"
201
+ local target_path="${worktree}/${relative_path}"
202
+
203
+ if [[ ! -d "$source_path" ]]; then
204
+ return 0
205
+ fi
206
+
207
+ if [[ -e "$target_path" ]]; then
208
+ return 0
209
+ fi
210
+
211
+ mkdir -p "$(dirname "$target_path")"
212
+ ln -s "$source_path" "$target_path"
213
+ echo "[agent-branch-start] Linked dependency dir in worktree: ${relative_path}"
214
+ }
215
+
216
+ initialize_openspec_plan_workspace() {
217
+ local repo="$1"
218
+ local worktree="$2"
219
+ local plan_slug="$3"
220
+
221
+ hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
222
+
223
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
224
+ return 0
225
+ fi
226
+
227
+ local openspec_script="${worktree}/scripts/openspec/init-plan-workspace.sh"
228
+ if [[ ! -f "$openspec_script" ]]; then
229
+ echo "[agent-branch-start] OpenSpec init script is missing in sandbox worktree." >&2
230
+ echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2
231
+ return 1
232
+ fi
233
+ if [[ ! -x "$openspec_script" ]]; then
234
+ chmod +x "$openspec_script" 2>/dev/null || true
235
+ fi
236
+
237
+ local init_output=""
238
+ if ! init_output="$(
239
+ cd "$worktree"
240
+ bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
241
+ )"; then
242
+ printf '%s\n' "$init_output" >&2
243
+ echo "[agent-branch-start] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
244
+ return 1
245
+ fi
246
+
247
+ if [[ -n "$init_output" ]]; then
248
+ printf '%s\n' "$init_output"
249
+ fi
250
+ echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}"
251
+ }
252
+
180
253
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
181
254
  echo "[agent-branch-start] Not inside a git repository." >&2
182
255
  exit 1
@@ -190,12 +263,15 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
190
263
  fi
191
264
 
192
265
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
193
- configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
194
- if [[ -n "$configured_base" ]]; then
195
- BASE_BRANCH="$configured_base"
266
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
267
+ protected_branches_raw="$(resolve_protected_branches "$repo_root")"
268
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
269
+ BASE_BRANCH="$current_branch"
196
270
  else
197
- current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
198
- if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
271
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
272
+ if [[ -n "$configured_base" ]]; then
273
+ BASE_BRANCH="$configured_base"
274
+ elif [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
199
275
  BASE_BRANCH="$current_branch"
200
276
  else
201
277
  BASE_BRANCH="dev"
@@ -235,6 +311,7 @@ done
235
311
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
236
312
  mkdir -p "$worktree_root"
237
313
  worktree_path="${worktree_root}/${branch_name//\//__}"
314
+ openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
238
315
 
239
316
  if [[ -e "$worktree_path" ]]; then
240
317
  echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
@@ -243,10 +320,11 @@ fi
243
320
 
244
321
  auto_transfer_stash_ref=""
245
322
  auto_transfer_message=""
323
+ auto_transfer_source_branch=""
246
324
  current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
247
325
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
248
- if [[ "$current_branch" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_BRANCH" "$protected_branches_raw"; then
249
- if has_tracked_changes "$repo_root"; then
326
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
327
+ if has_local_changes "$repo_root"; then
250
328
  auto_transfer_message="musafety-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
251
329
  if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
252
330
  auto_transfer_stash_ref="$(
@@ -254,7 +332,8 @@ if [[ "$current_branch" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_
254
332
  | awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
255
333
  )"
256
334
  if [[ -n "$auto_transfer_stash_ref" ]]; then
257
- echo "[agent-branch-start] Detected local changes on protected base '${BASE_BRANCH}'. Moving them to '${branch_name}'..."
335
+ auto_transfer_source_branch="$current_branch"
336
+ echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
258
337
  fi
259
338
  fi
260
339
  fi
@@ -270,19 +349,28 @@ fi
270
349
  if [[ -n "$auto_transfer_stash_ref" ]]; then
271
350
  if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
272
351
  git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
273
- echo "[agent-branch-start] Moved local changes from '${BASE_BRANCH}' into '${branch_name}'."
352
+ transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
353
+ echo "[agent-branch-start] Moved local changes from '${transfer_label}' into '${branch_name}'."
274
354
  else
275
355
  echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
276
- echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${BASE_BRANCH}." >&2
356
+ transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
357
+ echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${transfer_label}." >&2
277
358
  echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
278
359
  exit 1
279
360
  fi
280
361
  fi
281
362
 
282
363
  hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-agent.sh"
364
+ hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
365
+ hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
366
+ hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
367
+ if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
368
+ exit 1
369
+ fi
283
370
 
284
371
  echo "[agent-branch-start] Created branch: ${branch_name}"
285
372
  echo "[agent-branch-start] Worktree: ${worktree_path}"
373
+ echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
286
374
  echo "[agent-branch-start] Next steps:"
287
375
  echo " cd \"${worktree_path}\""
288
376
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
@@ -9,6 +9,10 @@ DELETE_BRANCHES=0
9
9
  DELETE_REMOTE_BRANCHES=0
10
10
  ONLY_DIRTY_WORKTREES=0
11
11
  TARGET_BRANCH=""
12
+ IDLE_MINUTES=0
13
+ NOW_EPOCH_RAW="${MUSAFETY_PRUNE_NOW_EPOCH:-}"
14
+ IDLE_SECONDS=0
15
+ NOW_EPOCH=0
12
16
 
13
17
  if [[ -n "$BASE_BRANCH" ]]; then
14
18
  BASE_BRANCH_EXPLICIT=1
@@ -45,9 +49,13 @@ while [[ $# -gt 0 ]]; do
45
49
  TARGET_BRANCH="${2:-}"
46
50
  shift 2
47
51
  ;;
52
+ --idle-minutes)
53
+ IDLE_MINUTES="${2:-}"
54
+ shift 2
55
+ ;;
48
56
  *)
49
57
  echo "[agent-worktree-prune] Unknown argument: $1" >&2
50
- echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
58
+ echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
51
59
  exit 1
52
60
  ;;
53
61
  esac
@@ -103,6 +111,16 @@ if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
103
111
  exit 1
104
112
  fi
105
113
 
114
+ if [[ ! "$IDLE_MINUTES" =~ ^[0-9]+$ ]]; then
115
+ echo "[agent-worktree-prune] --idle-minutes must be an integer >= 0." >&2
116
+ exit 1
117
+ fi
118
+
119
+ if [[ -n "$NOW_EPOCH_RAW" && ! "$NOW_EPOCH_RAW" =~ ^[0-9]+$ ]]; then
120
+ echo "[agent-worktree-prune] MUSAFETY_PRUNE_NOW_EPOCH must be a unix timestamp integer." >&2
121
+ exit 1
122
+ fi
123
+
106
124
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
107
125
  BASE_BRANCH="$(resolve_base_branch)"
108
126
  fi
@@ -117,6 +135,13 @@ if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}";
117
135
  exit 1
118
136
  fi
119
137
 
138
+ IDLE_SECONDS=$((IDLE_MINUTES * 60))
139
+ if [[ -n "$NOW_EPOCH_RAW" ]]; then
140
+ NOW_EPOCH="$NOW_EPOCH_RAW"
141
+ else
142
+ NOW_EPOCH="$(date +%s)"
143
+ fi
144
+
120
145
  run_cmd() {
121
146
  if [[ "$DRY_RUN" -eq 1 ]]; then
122
147
  echo "[agent-worktree-prune] [dry-run] $*"
@@ -167,6 +192,71 @@ select_unique_worktree_path() {
167
192
  printf '%s' "$candidate"
168
193
  }
169
194
 
195
+ read_branch_activity_epoch() {
196
+ local branch="$1"
197
+ local wt="${2:-}"
198
+ local activity_epoch=""
199
+
200
+ activity_epoch="$(
201
+ git -C "$repo_root" reflog show --format='%ct' -n 1 "refs/heads/${branch}" 2>/dev/null \
202
+ | head -n 1 \
203
+ | tr -d '[:space:]'
204
+ )"
205
+ if [[ -z "$activity_epoch" ]]; then
206
+ activity_epoch="$(
207
+ git -C "$repo_root" log -1 --format='%ct' "$branch" 2>/dev/null \
208
+ | head -n 1 \
209
+ | tr -d '[:space:]'
210
+ )"
211
+ fi
212
+
213
+ if [[ -n "$wt" && -d "$wt" ]]; then
214
+ local lock_file="${wt}/.omx/state/agent-file-locks.json"
215
+ if [[ -f "$lock_file" ]]; then
216
+ local lock_mtime=""
217
+ lock_mtime="$(stat -c %Y "$lock_file" 2>/dev/null || stat -f %m "$lock_file" 2>/dev/null || true)"
218
+ if [[ "$lock_mtime" =~ ^[0-9]+$ ]]; then
219
+ if [[ -z "$activity_epoch" || "$lock_mtime" -gt "$activity_epoch" ]]; then
220
+ activity_epoch="$lock_mtime"
221
+ fi
222
+ fi
223
+ fi
224
+ fi
225
+
226
+ printf '%s' "$activity_epoch"
227
+ }
228
+
229
+ skipped_recent=0
230
+
231
+ branch_idle_gate() {
232
+ local branch="$1"
233
+ local wt="$2"
234
+ local reason="$3"
235
+ if [[ "$IDLE_SECONDS" -le 0 ]]; then
236
+ return 0
237
+ fi
238
+ if [[ -z "$branch" ]]; then
239
+ return 0
240
+ fi
241
+
242
+ local last_activity_epoch=""
243
+ last_activity_epoch="$(read_branch_activity_epoch "$branch" "$wt")"
244
+ if [[ ! "$last_activity_epoch" =~ ^[0-9]+$ ]]; then
245
+ return 0
246
+ fi
247
+
248
+ local idle_age=$((NOW_EPOCH - last_activity_epoch))
249
+ if [[ "$idle_age" -lt 0 ]]; then
250
+ idle_age=0
251
+ fi
252
+ if [[ "$idle_age" -lt "$IDLE_SECONDS" ]]; then
253
+ skipped_recent=$((skipped_recent + 1))
254
+ echo "[agent-worktree-prune] Skipping recent branch (${reason}): ${branch} (idle=${idle_age}s < ${IDLE_SECONDS}s)"
255
+ return 1
256
+ fi
257
+ return 0
258
+ }
259
+
170
260
  relocated_foreign=0
171
261
  skipped_foreign=0
172
262
 
@@ -273,6 +363,10 @@ process_entry() {
273
363
  return
274
364
  fi
275
365
 
366
+ if ! branch_idle_gate "$branch" "$wt" "$remove_reason"; then
367
+ return
368
+ fi
369
+
276
370
  if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
277
371
  skipped_dirty=$((skipped_dirty + 1))
278
372
  echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
@@ -339,6 +433,9 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
339
433
  if branch_has_worktree "$branch"; then
340
434
  continue
341
435
  fi
436
+ if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
437
+ continue
438
+ fi
342
439
  if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
343
440
  if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
344
441
  removed_branches=$((removed_branches + 1))
@@ -356,7 +453,7 @@ fi
356
453
 
357
454
  run_cmd git -C "$repo_root" worktree prune
358
455
 
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}"
456
+ echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, idle_minutes=${IDLE_MINUTES}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}, skipped_recent=${skipped_recent}"
360
457
  if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
361
458
  echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
362
459
  fi
@@ -366,3 +463,6 @@ fi
366
463
  if [[ "$skipped_dirty" -gt 0 ]]; then
367
464
  echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
368
465
  fi
466
+ if [[ "$IDLE_SECONDS" -gt 0 && "$skipped_recent" -gt 0 ]]; then
467
+ echo "[agent-worktree-prune] Tip: recent branches were preserved by --idle-minutes=${IDLE_MINUTES}. Re-run later or lower the threshold." >&2
468
+ fi
@@ -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
@@ -136,6 +139,46 @@ sanitize_slug() {
136
139
  printf '%s' "$slug"
137
140
  }
138
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
+
139
182
  resolve_start_base_branch() {
140
183
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
141
184
  printf '%s' "$BASE_BRANCH"
@@ -239,7 +282,7 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
239
282
  start_output=""
240
283
  start_status=0
241
284
  set +e
242
- start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
285
+ start_output="$(MUSAFETY_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
243
286
  start_status=$?
244
287
  set -e
245
288
 
@@ -363,6 +406,43 @@ sync_worktree_with_base() {
363
406
  return 0
364
407
  }
365
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
+
366
446
  worktree_has_changes() {
367
447
  local wt="$1"
368
448
  if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
@@ -579,6 +659,16 @@ if ! sync_worktree_with_base "$worktree_path"; then
579
659
  exit 1
580
660
  fi
581
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
+
582
672
  echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
583
673
  cd "$worktree_path"
584
674
  set +e
@@ -590,8 +680,6 @@ cd "$repo_root"
590
680
  final_exit="$codex_exit"
591
681
  auto_finish_completed=0
592
682
 
593
- worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
594
-
595
683
  if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
596
684
  if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 && "$AUTO_CLEANUP" -eq 1 ]]; then
597
685
  echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge -> cleanup."