@imdeadpool/guardex 5.0.12 → 5.0.15

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.12",
3
+ "version": "5.0.15",
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,
@@ -16,8 +16,8 @@
16
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`).
17
17
  - Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
18
18
  - Use `--no-cleanup` only when you explicitly need to keep a merged sandbox for audit/debug follow-up.
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.
20
- - 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
+ - If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --base dev --via-pr --wait-for-merge` and keep the branch open until checks/review pass.
20
+ - If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --base dev --via-pr --wait-for-merge` until merged.
21
21
  - 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`.
22
22
  - For every new task, including follow-up work in the same chat/session, 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`.
23
23
  - Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
@@ -46,12 +46,13 @@
46
46
  - Verification commands + results
47
47
  - Risks / follow-ups
48
48
 
49
- ## OpenSpec Plan Workspace (required for agent sub-branch changes)
49
+ ## OpenSpec Workspaces (required for agent sub-branch changes)
50
50
 
51
- OMX Codex execution flows must use OpenSpec. `scripts/codex-agent.sh` bootstraps a
52
- per-branch plan workspace automatically under:
51
+ OMX Codex execution flows must use OpenSpec. `scripts/codex-agent.sh` bootstraps
52
+ per-branch OpenSpec workspaces automatically:
53
53
 
54
54
  ```text
55
+ openspec/changes/<agent-branch-slug>/
55
56
  openspec/plan/<agent-branch-slug>/
56
57
  ```
57
58
 
@@ -59,10 +60,21 @@ For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with
59
60
  `MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
60
61
 
61
62
  ```bash
63
+ bash scripts/openspec/init-change-workspace.sh "<change-slug>" "<capability-slug>"
62
64
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
63
65
  ```
64
66
 
65
- Expected shape:
67
+ Expected change shape:
68
+
69
+ ```text
70
+ openspec/changes/<change-slug>/
71
+ .openspec.yaml
72
+ proposal.md
73
+ tasks.md
74
+ specs/<capability-slug>/spec.md
75
+ ```
76
+
77
+ Expected plan shape:
66
78
 
67
79
  ```text
68
80
  openspec/plan/<plan-slug>/
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ "${MUSAFETY_DISABLE_POST_MERGE_CLEANUP:-0}" == "1" ]]; then
5
+ exit 0
6
+ fi
7
+
8
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
9
+ if [[ -z "$repo_root" ]]; then
10
+ exit 0
11
+ fi
12
+
13
+ branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
14
+ if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
15
+ exit 0
16
+ fi
17
+
18
+ base_branch="${MUSAFETY_BASE_BRANCH:-$(git -C "$repo_root" config --get multiagent.baseBranch || true)}"
19
+ if [[ -z "$base_branch" ]]; then
20
+ base_branch="dev"
21
+ fi
22
+
23
+ if [[ "$branch" != "$base_branch" ]]; then
24
+ exit 0
25
+ fi
26
+
27
+ cli_path="$repo_root/bin/multiagent-safety.js"
28
+ if [[ ! -f "$cli_path" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ node_bin="${MUSAFETY_NODE_BIN:-node}"
33
+ if ! command -v "$node_bin" >/dev/null 2>&1; then
34
+ exit 0
35
+ fi
36
+
37
+ "$node_bin" "$cli_path" cleanup \
38
+ --target "$repo_root" \
39
+ --base "$base_branch" \
40
+ --include-pr-merged \
41
+ --keep-clean-worktrees >/dev/null 2>&1 || true
42
+
43
+ exit 0
@@ -30,7 +30,7 @@ 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="true"
33
+ allow_vscode_protected_raw="false"
34
34
  fi
35
35
  allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
36
36
 
@@ -55,15 +55,6 @@ 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
-
67
58
  codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
68
59
  if [[ -z "$codex_require_agent_branch_raw" ]]; then
69
60
  codex_require_agent_branch_raw="true"
@@ -134,7 +125,7 @@ fi
134
125
 
135
126
  if [[ "$is_protected_branch" == "1" ]]; then
136
127
  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
128
+ if [[ "$allow_vscode_protected_branch_writes" == "1" ]]; then
138
129
  exit 0
139
130
  fi
140
131
  fi
@@ -155,11 +146,21 @@ Use an agent branch first:
155
146
  After finishing work:
156
147
  bash scripts/agent-branch-finish.sh
157
148
 
158
- Optional repo hard-block for VS Code protected-branch commits:
159
- git config multiagent.allowVscodeProtectedBranchWrites false
149
+ Optional repo opt-in for VS Code protected-branch commits:
150
+ git config multiagent.allowVscodeProtectedBranchWrites true
151
+
152
+ Temporary bypass (not recommended):
153
+ ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
154
+ MSG
155
+ exit 1
156
+ fi
160
157
 
161
- VS Code Source Control commits on protected local-only branches
162
- (no upstream and no remote branch) are allowed automatically.
158
+ if [[ "$is_agent_context" == "1" && "$branch" != agent/* ]]; then
159
+ cat >&2 <<'MSG'
160
+ [agent-branch-guard] Agent commits must run on dedicated agent/* branches.
161
+ Start an agent branch first:
162
+ bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
163
+ Then commit on that branch.
163
164
 
164
165
  Temporary bypass (not recommended):
165
166
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
@@ -168,6 +169,14 @@ MSG
168
169
  fi
169
170
 
170
171
  if [[ "$branch" == agent/* ]]; then
172
+ if [[ "${MUSAFETY_AUTOCLAIM_STAGED_LOCKS:-1}" == "1" ]]; then
173
+ while IFS= read -r staged_file; do
174
+ [[ -z "$staged_file" ]] && continue
175
+ [[ "$staged_file" == ".omx/state/agent-file-locks.json" ]] && continue
176
+ python3 scripts/agent-file-locks.py claim --branch "$branch" "$staged_file" >/dev/null 2>&1 || true
177
+ done < <(git diff --cached --name-only --diff-filter=ACMRDTUXB)
178
+ fi
179
+
171
180
  if ! python3 scripts/agent-file-locks.py validate --branch "$branch" --staged; then
172
181
  cat >&2 <<'MSG'
173
182
  [agent-branch-guard] Agent branch commits require file ownership locks.
@@ -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="true"
15
+ allow_vscode_protected_raw="false"
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 repo hard-block for VS Code protected-branch push:"
81
- echo " git config multiagent.allowVscodeProtectedBranchWrites false"
80
+ echo "[agent-branch-guard] Optional repo opt-in for VS Code protected-branch push:"
81
+ echo " git config multiagent.allowVscodeProtectedBranchWrites true"
82
82
  echo
83
83
  echo "Temporary bypass (not recommended):"
84
84
  echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
@@ -162,28 +162,6 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
162
162
  fi
163
163
  fi
164
164
 
165
- if [[ -z "$BASE_BRANCH" ]]; then
166
- branch_stored_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.musafetyBase" || true)"
167
- if [[ -n "$branch_stored_base" ]]; then
168
- BASE_BRANCH="$branch_stored_base"
169
- fi
170
- fi
171
-
172
- if [[ -z "$BASE_BRANCH" ]]; then
173
- source_upstream="$(git -C "$repo_root" for-each-ref --format='%(upstream:short)' "refs/heads/${SOURCE_BRANCH}" | head -n 1)"
174
- source_upstream="${source_upstream:-}"
175
- if [[ "$source_upstream" == */* ]]; then
176
- BASE_BRANCH="${source_upstream#*/}"
177
- fi
178
- fi
179
-
180
- if [[ -z "$BASE_BRANCH" ]]; then
181
- current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
182
- if [[ -n "$current_branch" && "$current_branch" != "HEAD" && "$current_branch" != "$SOURCE_BRANCH" ]]; then
183
- BASE_BRANCH="$current_branch"
184
- fi
185
- fi
186
-
187
165
  if [[ -z "$BASE_BRANCH" ]]; then
188
166
  BASE_BRANCH="dev"
189
167
  fi
@@ -8,6 +8,8 @@ BASE_BRANCH_EXPLICIT=0
8
8
  WORKTREE_ROOT_REL=".omx/agent-worktrees"
9
9
  OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-false}"
10
10
  OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
11
+ OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}"
12
+ OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}"
11
13
  POSITIONAL_ARGS=()
12
14
 
13
15
  while [[ $# -gt 0 ]]; do
@@ -109,6 +111,25 @@ resolve_openspec_plan_slug() {
109
111
  sanitize_slug "${branch_name//\//-}" "$task_slug"
110
112
  }
111
113
 
114
+ resolve_openspec_change_slug() {
115
+ local branch_name="$1"
116
+ local task_slug="$2"
117
+ if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then
118
+ sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug"
119
+ return 0
120
+ fi
121
+ sanitize_slug "${branch_name//\//-}" "$task_slug"
122
+ }
123
+
124
+ resolve_openspec_capability_slug() {
125
+ local task_slug="$1"
126
+ if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then
127
+ sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug"
128
+ return 0
129
+ fi
130
+ sanitize_slug "$task_slug" "general-behavior"
131
+ }
132
+
112
133
  resolve_active_codex_snapshot_name() {
113
134
  local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
114
135
  if [[ -n "$override" ]]; then
@@ -250,6 +271,44 @@ initialize_openspec_plan_workspace() {
250
271
  echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}"
251
272
  }
252
273
 
274
+ initialize_openspec_change_workspace() {
275
+ local repo="$1"
276
+ local worktree="$2"
277
+ local change_slug="$3"
278
+ local capability_slug="$4"
279
+
280
+ hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh"
281
+
282
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
283
+ return 0
284
+ fi
285
+
286
+ local openspec_script="${worktree}/scripts/openspec/init-change-workspace.sh"
287
+ if [[ ! -f "$openspec_script" ]]; then
288
+ echo "[agent-branch-start] OpenSpec change init script is missing in sandbox worktree." >&2
289
+ echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2
290
+ return 1
291
+ fi
292
+ if [[ ! -x "$openspec_script" ]]; then
293
+ chmod +x "$openspec_script" 2>/dev/null || true
294
+ fi
295
+
296
+ local init_output=""
297
+ if ! init_output="$(
298
+ cd "$worktree"
299
+ bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
300
+ )"; then
301
+ printf '%s\n' "$init_output" >&2
302
+ echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
303
+ return 1
304
+ fi
305
+
306
+ if [[ -n "$init_output" ]]; then
307
+ printf '%s\n' "$init_output"
308
+ fi
309
+ echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}"
310
+ }
311
+
253
312
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
254
313
  echo "[agent-branch-start] Not inside a git repository." >&2
255
314
  exit 1
@@ -312,6 +371,8 @@ worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
312
371
  mkdir -p "$worktree_root"
313
372
  worktree_path="${worktree_root}/${branch_name//\//__}"
314
373
  openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
374
+ openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
375
+ openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
315
376
 
316
377
  if [[ -e "$worktree_path" ]]; then
317
378
  echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
@@ -364,15 +425,19 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag
364
425
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
365
426
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
366
427
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
428
+ if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug"; then
429
+ exit 1
430
+ fi
367
431
  if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
368
432
  exit 1
369
433
  fi
370
434
 
371
435
  echo "[agent-branch-start] Created branch: ${branch_name}"
372
436
  echo "[agent-branch-start] Worktree: ${worktree_path}"
437
+ echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
373
438
  echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
374
439
  echo "[agent-branch-start] Next steps:"
375
440
  echo " cd \"${worktree_path}\""
376
441
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
377
442
  echo " # implement + commit"
378
- echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\""
443
+ echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base dev --via-pr --wait-for-merge"
@@ -12,6 +12,8 @@ AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}"
12
12
  AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}"
13
13
  OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}"
14
14
  OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
15
+ OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}"
16
+ OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}"
15
17
 
16
18
  normalize_bool() {
17
19
  local raw="${1:-}"
@@ -150,6 +152,27 @@ resolve_openspec_plan_slug() {
150
152
  sanitize_slug "${branch_name//\//-}" "$task_slug"
151
153
  }
152
154
 
155
+ resolve_openspec_change_slug() {
156
+ local branch_name="$1"
157
+ local task_slug
158
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
159
+ if [[ -n "$OPENSPEC_CHANGE_SLUG_OVERRIDE" ]]; then
160
+ sanitize_slug "$OPENSPEC_CHANGE_SLUG_OVERRIDE" "$task_slug"
161
+ return 0
162
+ fi
163
+ sanitize_slug "${branch_name//\//-}" "$task_slug"
164
+ }
165
+
166
+ resolve_openspec_capability_slug() {
167
+ local task_slug
168
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
169
+ if [[ -n "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" ]]; then
170
+ sanitize_slug "$OPENSPEC_CAPABILITY_SLUG_OVERRIDE" "$task_slug"
171
+ return 0
172
+ fi
173
+ sanitize_slug "$task_slug" "general-behavior"
174
+ }
175
+
153
176
  hydrate_local_helper_in_worktree() {
154
177
  local worktree="$1"
155
178
  local relative_path="$2"
@@ -192,13 +215,6 @@ resolve_start_base_branch() {
192
215
  return 0
193
216
  fi
194
217
 
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
218
  printf 'dev'
203
219
  }
204
220
 
@@ -338,30 +354,20 @@ has_origin_remote() {
338
354
  }
339
355
 
340
356
  resolve_worktree_base_branch() {
341
- local wt="$1"
357
+ local _wt="$1"
342
358
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
343
359
  printf '%s' "$BASE_BRANCH"
344
360
  return 0
345
361
  fi
346
362
 
347
- local branch
348
- branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
349
- if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
350
- return 0
351
- fi
352
-
353
- local stored_base
354
- stored_base="$(git -C "$repo_root" config --get "branch.${branch}.musafetyBase" || true)"
355
- if [[ -n "$stored_base" ]]; then
356
- printf '%s' "$stored_base"
357
- return 0
358
- fi
359
-
360
363
  local configured_base
361
364
  configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
362
365
  if [[ -n "$configured_base" ]]; then
363
366
  printf '%s' "$configured_base"
367
+ return 0
364
368
  fi
369
+
370
+ printf 'dev'
365
371
  }
366
372
 
367
373
  sync_worktree_with_base() {
@@ -443,6 +449,43 @@ ensure_openspec_plan_workspace() {
443
449
  echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}"
444
450
  }
445
451
 
452
+ ensure_openspec_change_workspace() {
453
+ local wt="$1"
454
+ local branch="$2"
455
+
456
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
457
+ return 0
458
+ fi
459
+
460
+ hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-change-workspace.sh"
461
+
462
+ local openspec_script="${wt}/scripts/openspec/init-change-workspace.sh"
463
+ if [[ ! -f "$openspec_script" ]]; then
464
+ echo "[codex-agent] Missing OpenSpec change init script in sandbox: ${openspec_script}" >&2
465
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
466
+ return 1
467
+ fi
468
+ if [[ ! -x "$openspec_script" ]]; then
469
+ chmod +x "$openspec_script" 2>/dev/null || true
470
+ fi
471
+
472
+ local change_slug capability_slug init_output=""
473
+ change_slug="$(resolve_openspec_change_slug "$branch")"
474
+ capability_slug="$(resolve_openspec_capability_slug)"
475
+ if ! init_output="$(
476
+ cd "$wt"
477
+ bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
478
+ )"; then
479
+ printf '%s\n' "$init_output" >&2
480
+ echo "[codex-agent] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
481
+ return 1
482
+ fi
483
+ if [[ -n "$init_output" ]]; then
484
+ printf '%s\n' "$init_output"
485
+ fi
486
+ echo "[codex-agent] OpenSpec change workspace: ${wt}/openspec/changes/${change_slug}"
487
+ }
488
+
446
489
  worktree_has_changes() {
447
490
  local wt="$1"
448
491
  if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
@@ -598,12 +641,18 @@ looks_like_conflict_failure() {
598
641
  run_finish_flow() {
599
642
  local wt="$1"
600
643
  local branch="$2"
644
+ local finish_base_branch=""
601
645
  local finish_output=""
602
646
  local -a finish_args
603
647
 
604
648
  finish_args=(--branch "$branch")
605
- if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
606
- finish_args+=(--base "$BASE_BRANCH")
649
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
650
+ finish_base_branch="$BASE_BRANCH"
651
+ else
652
+ finish_base_branch="$(resolve_worktree_base_branch "$wt")"
653
+ fi
654
+ if [[ -n "$finish_base_branch" ]]; then
655
+ finish_args+=(--base "$finish_base_branch")
607
656
  fi
608
657
  if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
609
658
  finish_args+=(--cleanup)
@@ -613,9 +662,11 @@ run_finish_flow() {
613
662
  fi
614
663
 
615
664
  if has_origin_remote; then
616
- if command -v gh >/dev/null 2>&1 || command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1; then
617
- finish_args+=(--via-pr)
665
+ if ! command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1 && ! command -v gh >/dev/null 2>&1; then
666
+ echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${MUSAFETY_GH_BIN:-gh}" >&2
667
+ return 2
618
668
  fi
669
+ finish_args+=(--via-pr)
619
670
  else
620
671
  echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
621
672
  return 2
@@ -631,7 +682,7 @@ run_finish_flow() {
631
682
  if [[ "$AUTO_REVIEW_ON_CONFLICT" -eq 1 ]] && looks_like_conflict_failure "$finish_output"; then
632
683
  echo "[codex-agent] Auto-finish hit conflicts. Launching Codex conflict-review pass in sandbox..." >&2
633
684
  local review_prompt
634
- review_prompt="Resolve git conflicts for branch ${branch} against ${BASE_BRANCH:-base branch}, then commit the resolution in this sandbox worktree and exit."
685
+ review_prompt="Resolve git conflicts for branch ${branch} against ${finish_base_branch:-dev}, then commit the resolution in this sandbox worktree and exit."
635
686
 
636
687
  (
637
688
  cd "$wt"
@@ -665,6 +716,10 @@ if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then
665
716
  exit 1
666
717
  fi
667
718
 
719
+ if ! ensure_openspec_change_workspace "$worktree_path" "$worktree_branch"; then
720
+ exit 1
721
+ fi
722
+
668
723
  if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
669
724
  exit 1
670
725
  fi
@@ -735,7 +790,7 @@ else
735
790
  if [[ "$auto_finish_completed" -eq 1 ]]; then
736
791
  echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
737
792
  else
738
- echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --via-pr"
793
+ echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --base dev --via-pr --wait-for-merge"
739
794
  echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
740
795
  fi
741
796
  fi
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ $# -lt 1 || $# -gt 2 ]]; then
5
+ echo "Usage: $0 <change-slug> [capability-slug]"
6
+ echo "Example: $0 add-dashboard-live-usage runtime-migration"
7
+ exit 1
8
+ fi
9
+
10
+ CHANGE_SLUG="$1"
11
+ CAPABILITY_SLUG="${2:-$CHANGE_SLUG}"
12
+
13
+ if [[ "$CHANGE_SLUG" =~ [^a-z0-9-] ]]; then
14
+ echo "Error: change slug must be kebab-case (lowercase letters, numbers, hyphens)."
15
+ exit 1
16
+ fi
17
+
18
+ if [[ "$CAPABILITY_SLUG" =~ [^a-z0-9-] ]]; then
19
+ echo "Error: capability slug must be kebab-case (lowercase letters, numbers, hyphens)."
20
+ exit 1
21
+ fi
22
+
23
+ CHANGE_DIR="openspec/changes/${CHANGE_SLUG}"
24
+ SPEC_DIR="${CHANGE_DIR}/specs/${CAPABILITY_SLUG}"
25
+ TODAY="$(date -u +%Y-%m-%d)"
26
+
27
+ mkdir -p "$SPEC_DIR"
28
+
29
+ if [[ ! -f "${CHANGE_DIR}/.openspec.yaml" ]]; then
30
+ cat > "${CHANGE_DIR}/.openspec.yaml" <<YAMLEOF
31
+ schema: spec-driven
32
+ created: ${TODAY}
33
+ YAMLEOF
34
+ fi
35
+
36
+ if [[ ! -f "${CHANGE_DIR}/proposal.md" ]]; then
37
+ cat > "${CHANGE_DIR}/proposal.md" <<PROPOSALEOF
38
+ ## Why
39
+
40
+ - TODO: describe the user/problem outcome this change addresses.
41
+
42
+ ## What Changes
43
+
44
+ - TODO: summarize the intended behavior and scope.
45
+
46
+ ## Impact
47
+
48
+ - TODO: call out risks, rollout notes, and affected surfaces.
49
+ PROPOSALEOF
50
+ fi
51
+
52
+ if [[ ! -f "${CHANGE_DIR}/tasks.md" ]]; then
53
+ cat > "${CHANGE_DIR}/tasks.md" <<TASKSEOF
54
+ ## 1. Specification
55
+
56
+ - [ ] 1.1 Finalize proposal scope and acceptance criteria for \`${CHANGE_SLUG}\`.
57
+ - [ ] 1.2 Define normative requirements in \`specs/${CAPABILITY_SLUG}/spec.md\`.
58
+
59
+ ## 2. Implementation
60
+
61
+ - [ ] 2.1 Implement scoped behavior changes.
62
+ - [ ] 2.2 Add/update focused regression coverage.
63
+
64
+ ## 3. Verification
65
+
66
+ - [ ] 3.1 Run targeted project verification commands.
67
+ - [ ] 3.2 Run \`openspec validate ${CHANGE_SLUG} --type change --strict\`.
68
+ - [ ] 3.3 Run \`openspec validate --specs\`.
69
+ TASKSEOF
70
+ fi
71
+
72
+ if [[ ! -f "${SPEC_DIR}/spec.md" ]]; then
73
+ cat > "${SPEC_DIR}/spec.md" <<SPECEOF
74
+ ## ADDED Requirements
75
+
76
+ ### Requirement: ${CAPABILITY_SLUG} behavior
77
+ The system SHALL enforce ${CAPABILITY_SLUG} behavior as defined by this change.
78
+
79
+ #### Scenario: Baseline acceptance
80
+ - **WHEN** ${CAPABILITY_SLUG} behavior is exercised
81
+ - **THEN** the expected outcome is produced
82
+ - **AND** regressions are covered by tests.
83
+ SPECEOF
84
+ fi
85
+
86
+ echo "[guardex] OpenSpec change workspace ready: ${CHANGE_DIR}"
87
+ echo "[guardex] OpenSpec change spec scaffold: ${SPEC_DIR}/spec.md"