@imdeadpool/guardex 5.0.17 → 6.0.1

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.
@@ -6,10 +6,17 @@ 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:-}"
11
- OPENSPEC_CHANGE_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CHANGE_SLUG:-}"
12
- OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_CAPABILITY_SLUG:-}"
9
+ OPENSPEC_AUTO_INIT_RAW="${GX_OPENSPEC_AUTO_INIT:-${GUARDEX_OPENSPEC_AUTO_INIT:-true}}"
10
+ TIER_LEVEL_RAW="${GUARDEX_TIER:-}"
11
+ TIER_LEVEL_EXPLICIT=0
12
+ GH_SYNC_ON_START_RAW="${GUARDEX_GH_SYNC_ON_START:-true}"
13
+ MIGRATE_PROTECTED_CHANGES_RAW="${GUARDEX_MIGRATE_PROTECTED_CHANGES:-true}"
14
+ OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
15
+ OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
16
+ OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
17
+ PR_REF="${GUARDEX_GH_PR_REF:-}"
18
+ GH_REPO_REF="${GUARDEX_GH_REPO:-}"
19
+ PRINT_NAME_ONLY=0
13
20
  POSITIONAL_ARGS=()
14
21
 
15
22
  while [[ $# -gt 0 ]]; do
@@ -36,6 +43,31 @@ while [[ $# -gt 0 ]]; do
36
43
  WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
37
44
  shift 2
38
45
  ;;
46
+ --pr)
47
+ PR_REF="${2:-}"
48
+ shift 2
49
+ ;;
50
+ --repo)
51
+ GH_REPO_REF="${2:-}"
52
+ shift 2
53
+ ;;
54
+ --gh-sync)
55
+ GH_SYNC_ON_START_RAW="true"
56
+ shift
57
+ ;;
58
+ --no-gh-sync)
59
+ GH_SYNC_ON_START_RAW="false"
60
+ shift
61
+ ;;
62
+ --print-name-only)
63
+ PRINT_NAME_ONLY=1
64
+ shift
65
+ ;;
66
+ --tier)
67
+ TIER_LEVEL_RAW="${2:-}"
68
+ TIER_LEVEL_EXPLICIT=1
69
+ shift 2
70
+ ;;
39
71
  --)
40
72
  shift
41
73
  while [[ $# -gt 0 ]]; do
@@ -46,7 +78,7 @@ while [[ $# -gt 0 ]]; do
46
78
  ;;
47
79
  -*)
48
80
  echo "[agent-branch-start] Unknown option: $1" >&2
49
- echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
81
+ echo "Usage: $0 [task] [agent] [base] [--tier T0|T1|T2|T3] [--worktree-root <path>] [--pr <ref>] [--repo <owner/name>] [--gh-sync|--no-gh-sync]" >&2
50
82
  exit 1
51
83
  ;;
52
84
  *)
@@ -58,7 +90,7 @@ done
58
90
 
59
91
  if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
60
92
  echo "[agent-branch-start] Too many positional arguments." >&2
61
- echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
93
+ echo "Usage: $0 [task] [agent] [base] [--tier T0|T1|T2|T3] [--worktree-root <path>] [--pr <ref>] [--repo <owner/name>] [--gh-sync|--no-gh-sync]" >&2
62
94
  exit 1
63
95
  fi
64
96
 
@@ -86,6 +118,169 @@ sanitize_slug() {
86
118
  printf '%s' "$slug"
87
119
  }
88
120
 
121
+ sanitize_optional_slug() {
122
+ local raw="$1"
123
+ local fallback="${2:-snapshot}"
124
+ if [[ -z "$raw" ]]; then
125
+ printf ''
126
+ return 0
127
+ fi
128
+ sanitize_slug "$raw" "$fallback"
129
+ }
130
+
131
+ normalize_positive_int() {
132
+ local raw="$1"
133
+ local fallback="$2"
134
+ if [[ "$raw" =~ ^[0-9]+$ ]] && [[ "$raw" -gt 0 ]]; then
135
+ printf '%s' "$raw"
136
+ return 0
137
+ fi
138
+ printf '%s' "$fallback"
139
+ }
140
+
141
+ shorten_slug() {
142
+ local slug="$1"
143
+ local raw_max="$2"
144
+ local max_len
145
+ max_len="$(normalize_positive_int "$raw_max" "32")"
146
+ if [[ "${#slug}" -le "$max_len" ]]; then
147
+ printf '%s' "$slug"
148
+ return 0
149
+ fi
150
+ local shortened="${slug:0:max_len}"
151
+ shortened="$(printf '%s' "$shortened" | sed -E 's/-+$//')"
152
+ if [[ -z "$shortened" ]]; then
153
+ shortened="${slug:0:max_len}"
154
+ fi
155
+ printf '%s' "$shortened"
156
+ }
157
+
158
+ checksum_slug_suffix() {
159
+ local raw="$1"
160
+ local checksum
161
+ checksum="$(printf '%s' "$raw" | cksum | awk '{print $1}')"
162
+ printf '%s' "${checksum:0:6}"
163
+ }
164
+
165
+ compose_branch_descriptor() {
166
+ local task_slug="$1"
167
+ local task_max task_part checksum_part
168
+ task_max="$(normalize_positive_int "${GUARDEX_BRANCH_TASK_SLUG_MAX:-36}" "36")"
169
+ task_part="$(shorten_slug "$task_slug" "$task_max")"
170
+ checksum_part="$(checksum_slug_suffix "$task_slug")"
171
+ printf '%s-%s' "$task_part" "$checksum_part"
172
+ }
173
+
174
+ normalize_agent_type() {
175
+ local raw="${1:-}"
176
+ local lowered
177
+ if [[ -n "${GUARDEX_AGENT_TYPE:-}" ]]; then
178
+ printf '%s' "${GUARDEX_AGENT_TYPE}"
179
+ return 0
180
+ fi
181
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
182
+ if [[ "$lowered" == *codex* ]]; then
183
+ printf 'codex'
184
+ return 0
185
+ fi
186
+ if [[ "$lowered" == *claude* ]]; then
187
+ printf 'claude'
188
+ return 0
189
+ fi
190
+ if [[ -n "${CLAUDECODE:-}" || -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
191
+ printf 'claude'
192
+ return 0
193
+ fi
194
+ if [[ -n "${CODEX_CLI:-}" ]]; then
195
+ printf 'codex'
196
+ return 0
197
+ fi
198
+ printf 'codex'
199
+ }
200
+
201
+ lookup_nickname_map() {
202
+ local key="$1"
203
+ local map_env="${GUARDEX_EMAIL_NICKNAME_MAP:-}"
204
+ local map_file="${repo_root:-$(pwd)}/.agents/nickname-map.json"
205
+ local value=""
206
+ if ! command -v python3 >/dev/null 2>&1; then
207
+ printf ''
208
+ return 0
209
+ fi
210
+ if [[ -n "$map_env" ]]; then
211
+ value="$(KEY="$key" MAP="$map_env" python3 -c '
212
+ import json, os
213
+ try:
214
+ data = json.loads(os.environ.get("MAP") or "{}")
215
+ print(data.get(os.environ.get("KEY", ""), ""))
216
+ except Exception:
217
+ pass
218
+ ' 2>/dev/null || true)"
219
+ fi
220
+ if [[ -z "$value" && -f "$map_file" ]]; then
221
+ value="$(KEY="$key" FILE="$map_file" python3 -c '
222
+ import json, os
223
+ try:
224
+ with open(os.environ["FILE"]) as f:
225
+ data = json.load(f)
226
+ print(data.get(os.environ.get("KEY", ""), ""))
227
+ except Exception:
228
+ pass
229
+ ' 2>/dev/null || true)"
230
+ fi
231
+ printf '%s' "$value"
232
+ }
233
+
234
+ time_fallback_nickname() {
235
+ # Current wall-clock HH-MM used when no codex-auth nickname is resolvable
236
+ # (typical for Claude sessions). Produces visually distinct, sortable
237
+ # branch slugs instead of the generic "none" marker. Tests override via
238
+ # GUARDEX_NICKNAME_TIME_FALLBACK.
239
+ if [[ -n "${GUARDEX_NICKNAME_TIME_FALLBACK:-}" ]]; then
240
+ printf '%s' "${GUARDEX_NICKNAME_TIME_FALLBACK}"
241
+ return 0
242
+ fi
243
+ printf '%s' "$(date +%H-%M)"
244
+ }
245
+
246
+ extract_nickname() {
247
+ local raw="${1:-}"
248
+ local lowered candidate mapped fallback use_account_raw use_account
249
+ fallback="$(time_fallback_nickname)"
250
+ # Default: always use the HH-MM time fallback so agent branches are
251
+ # distinguishable by creation time (e.g. `agent/claude-00-38/…`) instead of
252
+ # reflecting whichever codex-auth account happens to be active. Opt back in
253
+ # to the old account-nickname behavior with GUARDEX_USE_ACCOUNT_NICKNAME=1.
254
+ use_account_raw="${GUARDEX_USE_ACCOUNT_NICKNAME:-0}"
255
+ use_account="$(printf '%s' "$use_account_raw" | tr '[:upper:]' '[:lower:]')"
256
+ case "$use_account" in
257
+ 1|true|yes|on) use_account=1 ;;
258
+ *) use_account=0 ;;
259
+ esac
260
+ if [[ "$use_account" -ne 1 || -z "$raw" ]]; then
261
+ printf '%s' "$fallback"
262
+ return 0
263
+ fi
264
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
265
+ mapped="$(lookup_nickname_map "$lowered")"
266
+ if [[ -n "$mapped" ]]; then
267
+ sanitize_slug "$mapped" "$fallback"
268
+ return 0
269
+ fi
270
+ if [[ "$lowered" == *"@"* ]]; then
271
+ candidate="${lowered%%@*}"
272
+ elif [[ "$lowered" == *-* ]]; then
273
+ candidate="${lowered%%-*}"
274
+ else
275
+ candidate="$lowered"
276
+ fi
277
+ candidate="$(sanitize_slug "$candidate" "$fallback")"
278
+ if [[ -z "$candidate" || "$candidate" == "none" ]]; then
279
+ candidate="$fallback"
280
+ fi
281
+ printf '%s' "$candidate"
282
+ }
283
+
89
284
  normalize_bool() {
90
285
  local raw="${1:-}"
91
286
  local fallback="${2:-0}"
@@ -100,6 +295,63 @@ normalize_bool() {
100
295
  }
101
296
 
102
297
  OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
298
+ GH_SYNC_ON_START="$(normalize_bool "$GH_SYNC_ON_START_RAW" "1")"
299
+ MIGRATE_PROTECTED_CHANGES="$(normalize_bool "$MIGRATE_PROTECTED_CHANGES_RAW" "1")"
300
+
301
+ normalize_tier() {
302
+ local raw="${1:-}"
303
+ local upper
304
+ upper="$(printf '%s' "$raw" | tr '[:lower:]' '[:upper:]')"
305
+ case "$upper" in
306
+ T0|T1|T2|T3) printf '%s' "$upper" ;;
307
+ 0) printf 'T0' ;;
308
+ 1) printf 'T1' ;;
309
+ 2) printf 'T2' ;;
310
+ 3) printf 'T3' ;;
311
+ '') printf 'T3' ;;
312
+ *)
313
+ echo "[agent-branch-start] Unknown tier: ${raw} (expected T0|T1|T2|T3)" >&2
314
+ return 1
315
+ ;;
316
+ esac
317
+ }
318
+
319
+ if ! TIER_LEVEL="$(normalize_tier "$TIER_LEVEL_RAW")"; then
320
+ exit 1
321
+ fi
322
+
323
+ case "$TIER_LEVEL" in
324
+ T0)
325
+ OPENSPEC_CHANGE_INIT=0
326
+ OPENSPEC_PLAN_INIT=0
327
+ OPENSPEC_CHANGE_MINIMAL=0
328
+ ;;
329
+ T1)
330
+ OPENSPEC_CHANGE_INIT=1
331
+ OPENSPEC_PLAN_INIT=0
332
+ OPENSPEC_CHANGE_MINIMAL=1
333
+ ;;
334
+ T2)
335
+ OPENSPEC_CHANGE_INIT=1
336
+ OPENSPEC_PLAN_INIT=0
337
+ OPENSPEC_CHANGE_MINIMAL=0
338
+ ;;
339
+ T3)
340
+ OPENSPEC_CHANGE_INIT=1
341
+ OPENSPEC_PLAN_INIT=1
342
+ OPENSPEC_CHANGE_MINIMAL=0
343
+ ;;
344
+ esac
345
+
346
+ # Explicit tier overrides the legacy OPENSPEC_AUTO_INIT env var; otherwise keep legacy behavior.
347
+ if [[ "$TIER_LEVEL_EXPLICIT" -eq 1 ]]; then
348
+ OPENSPEC_AUTO_INIT="$OPENSPEC_CHANGE_INIT"
349
+ fi
350
+
351
+ is_helper_agent_base_branch() {
352
+ local base_branch="$1"
353
+ [[ "$base_branch" == agent/* ]]
354
+ }
103
355
 
104
356
  resolve_openspec_plan_slug() {
105
357
  local branch_name="$1"
@@ -131,13 +383,13 @@ resolve_openspec_capability_slug() {
131
383
  }
132
384
 
133
385
  resolve_active_codex_snapshot_name() {
134
- local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
386
+ local override="${GUARDEX_CODEX_AUTH_SNAPSHOT:-}"
135
387
  if [[ -n "$override" ]]; then
136
388
  printf '%s' "$override"
137
389
  return 0
138
390
  fi
139
391
 
140
- local codex_auth_bin="${MUSAFETY_CODEX_AUTH_BIN:-codex-auth}"
392
+ local codex_auth_bin="${GUARDEX_CODEX_AUTH_BIN:-codex-auth}"
141
393
  if ! command -v "$codex_auth_bin" >/dev/null 2>&1; then
142
394
  return 0
143
395
  fi
@@ -162,10 +414,58 @@ has_local_changes() {
162
414
  return 1
163
415
  }
164
416
 
417
+ migrate_local_changes_to_worktree() {
418
+ local root="$1"
419
+ local source_branch="$2"
420
+ local target_branch="$3"
421
+ local target_worktree="$4"
422
+ local stash_label stash_output stash_status apply_output apply_status
423
+ local stash_ref="stash@{0}"
424
+
425
+ if ! has_local_changes "$root"; then
426
+ return 0
427
+ fi
428
+
429
+ stash_label="guardex-migrate-${source_branch//\//-}-to-${target_branch//\//-}-$(date -u +%Y%m%dT%H%M%SZ)"
430
+
431
+ set +e
432
+ stash_output="$(git -C "$root" stash push --include-untracked --message "$stash_label" 2>&1)"
433
+ stash_status=$?
434
+ set -e
435
+ if [[ "$stash_status" -ne 0 ]]; then
436
+ echo "[agent-branch-start] Failed to stash protected-branch changes before migration." >&2
437
+ printf '%s\n' "$stash_output" >&2
438
+ return 1
439
+ fi
440
+
441
+ if printf '%s\n' "$stash_output" | grep -qi "No local changes to save"; then
442
+ return 0
443
+ fi
444
+
445
+ set +e
446
+ apply_output="$(git -C "$target_worktree" stash apply --index "$stash_ref" 2>&1)"
447
+ apply_status=$?
448
+ set -e
449
+ if [[ "$apply_status" -ne 0 ]]; then
450
+ echo "[agent-branch-start] Created stash ${stash_ref} but could not auto-apply into worktree '${target_worktree}'." >&2
451
+ printf '%s\n' "$apply_output" >&2
452
+ echo "[agent-branch-start] Manual recovery options:" >&2
453
+ echo " # apply in sandbox worktree" >&2
454
+ echo " git -C \"$target_worktree\" stash apply --index ${stash_ref}" >&2
455
+ echo " # or restore on primary checkout if needed" >&2
456
+ echo " git -C \"$root\" stash apply --index ${stash_ref}" >&2
457
+ return 1
458
+ fi
459
+
460
+ git -C "$root" stash drop "$stash_ref" >/dev/null 2>&1 || true
461
+ echo "[agent-branch-start] Migrated local changes from '${source_branch}' into '${target_branch}' (${target_worktree})."
462
+ return 0
463
+ }
464
+
165
465
  resolve_protected_branches() {
166
466
  local root="$1"
167
467
  local raw
168
- raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git -C "$root" config --get multiagent.protectedBranches || true)}"
468
+ raw="${GUARDEX_PROTECTED_BRANCHES:-$(git -C "$root" config --get multiagent.protectedBranches || true)}"
169
469
  if [[ -z "$raw" ]]; then
170
470
  raw="dev main master"
171
471
  fi
@@ -184,6 +484,51 @@ is_protected_branch_name() {
184
484
  return 1
185
485
  }
186
486
 
487
+ branch_exists_locally_or_on_origin() {
488
+ local root="$1"
489
+ local branch="$2"
490
+ if git -C "$root" show-ref --verify --quiet "refs/heads/${branch}"; then
491
+ return 0
492
+ fi
493
+ if git -C "$root" show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
494
+ return 0
495
+ fi
496
+ return 1
497
+ }
498
+
499
+ resolve_default_base_branch_for_agent_subbranch() {
500
+ local root="$1"
501
+ local protected_raw="$2"
502
+ local configured_base candidate
503
+
504
+ configured_base="$(git -C "$root" config --get multiagent.baseBranch || true)"
505
+ if [[ -n "$configured_base" ]] && branch_exists_locally_or_on_origin "$root" "$configured_base"; then
506
+ printf '%s' "$configured_base"
507
+ return 0
508
+ fi
509
+
510
+ for candidate in $protected_raw; do
511
+ if branch_exists_locally_or_on_origin "$root" "$candidate"; then
512
+ printf '%s' "$candidate"
513
+ return 0
514
+ fi
515
+ done
516
+
517
+ if branch_exists_locally_or_on_origin "$root" "dev"; then
518
+ printf 'dev'
519
+ return 0
520
+ fi
521
+ if branch_exists_locally_or_on_origin "$root" "main"; then
522
+ printf 'main'
523
+ return 0
524
+ fi
525
+ if branch_exists_locally_or_on_origin "$root" "master"; then
526
+ printf 'master'
527
+ return 0
528
+ fi
529
+ return 1
530
+ }
531
+
187
532
  hydrate_local_helper_in_worktree() {
188
533
  local repo="$1"
189
534
  local worktree="$2"
@@ -241,7 +586,7 @@ initialize_openspec_plan_workspace() {
241
586
 
242
587
  hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
243
588
 
244
- if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
589
+ if [[ "${OPENSPEC_PLAN_INIT:-$OPENSPEC_AUTO_INIT}" -ne 1 ]]; then
245
590
  return 0
246
591
  fi
247
592
 
@@ -276,6 +621,7 @@ initialize_openspec_change_workspace() {
276
621
  local worktree="$2"
277
622
  local change_slug="$3"
278
623
  local capability_slug="$4"
624
+ local branch_name="${5:-}"
279
625
 
280
626
  hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh"
281
627
 
@@ -296,7 +642,8 @@ initialize_openspec_change_workspace() {
296
642
  local init_output=""
297
643
  if ! init_output="$(
298
644
  cd "$worktree"
299
- bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
645
+ GUARDEX_OPENSPEC_MINIMAL="${OPENSPEC_CHANGE_MINIMAL:-0}" \
646
+ bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" "$branch_name" 2>&1
300
647
  )"; then
301
648
  printf '%s\n' "$init_output" >&2
302
649
  echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
@@ -309,6 +656,459 @@ initialize_openspec_change_workspace() {
309
656
  echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}"
310
657
  }
311
658
 
659
+ filtered_status_output() {
660
+ local wt="$1"
661
+ git -C "$wt" status --porcelain --untracked-files=normal -- \
662
+ . \
663
+ ":(exclude).omx/state/agent-file-locks.json" \
664
+ ":(exclude).dev-ports.json" \
665
+ ":(exclude)apps/logs/*.log"
666
+ }
667
+
668
+ resolve_worktree_git_dir() {
669
+ local wt="$1"
670
+ local git_dir=""
671
+ git_dir="$(git -C "$wt" rev-parse --git-dir 2>/dev/null || true)"
672
+ if [[ -z "$git_dir" ]]; then
673
+ return 1
674
+ fi
675
+ if [[ "$git_dir" == /* ]]; then
676
+ git_dir="$(cd "$git_dir" 2>/dev/null && pwd -P || true)"
677
+ else
678
+ git_dir="$(cd "$wt/$git_dir" 2>/dev/null && pwd -P || true)"
679
+ fi
680
+ if [[ -z "$git_dir" ]]; then
681
+ return 1
682
+ fi
683
+ printf '%s' "$git_dir"
684
+ }
685
+
686
+ bootstrap_manifest_path_for_worktree() {
687
+ local wt="$1"
688
+ local git_dir=""
689
+ git_dir="$(resolve_worktree_git_dir "$wt" || true)"
690
+ if [[ -z "$git_dir" ]]; then
691
+ return 1
692
+ fi
693
+ printf '%s/guardex-bootstrap-manifest.json' "$git_dir"
694
+ }
695
+
696
+ record_worktree_bootstrap_manifest() {
697
+ local worktree="$1"
698
+ local branch="$2"
699
+ local base_branch="$3"
700
+ local change_slug="$4"
701
+ local plan_slug="$5"
702
+ local tier="${6:-T3}"
703
+ local manifest_path=""
704
+ local status_output=""
705
+
706
+ manifest_path="$(bootstrap_manifest_path_for_worktree "$worktree" || true)"
707
+ if [[ -z "$manifest_path" ]]; then
708
+ return 0
709
+ fi
710
+
711
+ status_output="$(filtered_status_output "$worktree")"
712
+ STATUS_OUTPUT="$status_output" python3 - "$worktree" "$manifest_path" "$branch" "$base_branch" "$change_slug" "$plan_slug" "$tier" <<'PY'
713
+ from __future__ import annotations
714
+
715
+ import hashlib
716
+ import json
717
+ import os
718
+ import sys
719
+ from datetime import datetime, timezone
720
+ from pathlib import Path
721
+
722
+
723
+ def parse_status_paths(raw: str) -> list[str]:
724
+ paths: list[str] = []
725
+ for line in raw.splitlines():
726
+ if len(line) < 4:
727
+ continue
728
+ path_part = line[3:]
729
+ if " -> " in path_part:
730
+ path_part = path_part.split(" -> ", 1)[1]
731
+ path_part = path_part.strip()
732
+ if path_part:
733
+ paths.append(path_part)
734
+ return sorted(set(paths))
735
+
736
+
737
+ def sha256_for_file(path: Path) -> str | None:
738
+ if not path.exists() or not path.is_file():
739
+ return None
740
+ digest = hashlib.sha256()
741
+ with path.open("rb") as handle:
742
+ for chunk in iter(lambda: handle.read(65536), b""):
743
+ digest.update(chunk)
744
+ return digest.hexdigest()
745
+
746
+
747
+ if len(sys.argv) != 8:
748
+ sys.exit(1)
749
+
750
+ worktree_root = Path(sys.argv[1])
751
+ manifest_path = Path(sys.argv[2])
752
+ branch_name = sys.argv[3]
753
+ base_branch = sys.argv[4]
754
+ change_slug = sys.argv[5]
755
+ plan_slug = sys.argv[6]
756
+ tier = sys.argv[7] or "T3"
757
+
758
+ status_paths = parse_status_paths(os.environ.get("STATUS_OUTPUT", ""))
759
+ entries: list[dict[str, object]] = []
760
+ for rel_path in status_paths:
761
+ file_path = worktree_root / rel_path
762
+ entries.append(
763
+ {
764
+ "path": rel_path,
765
+ "sha256": sha256_for_file(file_path),
766
+ }
767
+ )
768
+
769
+ payload = {
770
+ "version": 1,
771
+ "generatedAt": datetime.now(timezone.utc).isoformat(),
772
+ "branch": branch_name,
773
+ "baseBranch": base_branch,
774
+ "changeSlug": change_slug,
775
+ "planSlug": plan_slug,
776
+ "tier": tier,
777
+ "files": entries,
778
+ }
779
+
780
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
781
+ manifest_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
782
+ PY
783
+ echo "[agent-branch-start] Bootstrap manifest: ${manifest_path}"
784
+ }
785
+
786
+ worktree_matches_bootstrap_manifest() {
787
+ local worktree="$1"
788
+ local manifest_path=""
789
+ local status_output=""
790
+
791
+ manifest_path="$(bootstrap_manifest_path_for_worktree "$worktree" || true)"
792
+ if [[ -z "$manifest_path" || ! -f "$manifest_path" ]]; then
793
+ return 1
794
+ fi
795
+
796
+ status_output="$(filtered_status_output "$worktree")"
797
+ if [[ -z "$status_output" ]]; then
798
+ return 1
799
+ fi
800
+
801
+ STATUS_OUTPUT="$status_output" python3 - "$worktree" "$manifest_path" <<'PY'
802
+ from __future__ import annotations
803
+
804
+ import hashlib
805
+ import json
806
+ import os
807
+ import sys
808
+ from pathlib import Path
809
+
810
+
811
+ def parse_status_paths(raw: str) -> list[str]:
812
+ paths: list[str] = []
813
+ for line in raw.splitlines():
814
+ if len(line) < 4:
815
+ continue
816
+ path_part = line[3:]
817
+ if " -> " in path_part:
818
+ path_part = path_part.split(" -> ", 1)[1]
819
+ path_part = path_part.strip()
820
+ if path_part:
821
+ paths.append(path_part)
822
+ return sorted(set(paths))
823
+
824
+
825
+ def sha256_for_file(path: Path) -> str | None:
826
+ if not path.exists() or not path.is_file():
827
+ return None
828
+ digest = hashlib.sha256()
829
+ with path.open("rb") as handle:
830
+ for chunk in iter(lambda: handle.read(65536), b""):
831
+ digest.update(chunk)
832
+ return digest.hexdigest()
833
+
834
+
835
+ if len(sys.argv) != 3:
836
+ sys.exit(1)
837
+
838
+ worktree_root = Path(sys.argv[1])
839
+ manifest_path = Path(sys.argv[2])
840
+
841
+ status_paths = parse_status_paths(os.environ.get("STATUS_OUTPUT", ""))
842
+ if not status_paths:
843
+ sys.exit(1)
844
+
845
+ try:
846
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
847
+ except Exception:
848
+ sys.exit(1)
849
+
850
+ entries = payload.get("files")
851
+ if not isinstance(entries, list):
852
+ sys.exit(1)
853
+
854
+ manifest_by_path: dict[str, str | None] = {}
855
+ for entry in entries:
856
+ if not isinstance(entry, dict):
857
+ continue
858
+ path_value = entry.get("path")
859
+ if not isinstance(path_value, str) or not path_value:
860
+ continue
861
+ sha_value = entry.get("sha256")
862
+ if sha_value is not None and not isinstance(sha_value, str):
863
+ continue
864
+ manifest_by_path[path_value] = sha_value
865
+
866
+ if not manifest_by_path:
867
+ sys.exit(1)
868
+
869
+ for rel_path in status_paths:
870
+ if rel_path not in manifest_by_path:
871
+ sys.exit(1)
872
+ current_sha = sha256_for_file(worktree_root / rel_path)
873
+ if current_sha != manifest_by_path.get(rel_path):
874
+ sys.exit(1)
875
+
876
+ sys.exit(0)
877
+ PY
878
+ }
879
+
880
+ get_worktree_for_branch() {
881
+ local branch="$1"
882
+ git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
883
+ $1 == "worktree" { wt = $2 }
884
+ $1 == "branch" && $2 == target { print wt; exit }
885
+ '
886
+ }
887
+
888
+ is_clean_worktree() {
889
+ local wt="$1"
890
+ git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
891
+ && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
892
+ && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
893
+ }
894
+
895
+ json_escape() {
896
+ local raw="$1"
897
+ raw="${raw//\\/\\\\}"
898
+ raw="${raw//\"/\\\"}"
899
+ raw="${raw//$'\n'/\\n}"
900
+ printf '%s' "$raw"
901
+ }
902
+
903
+ link_worktree_mem0_compat_file() {
904
+ local mem0_file="$1"
905
+ local compat_file="$2"
906
+ local compat_target="$3"
907
+
908
+ mkdir -p "$(dirname "$compat_file")"
909
+ if [[ -e "$compat_file" ]]; then
910
+ return 0
911
+ fi
912
+
913
+ if ln -s "$compat_target" "$compat_file" >/dev/null 2>&1; then
914
+ return 0
915
+ fi
916
+
917
+ cp "$mem0_file" "$compat_file"
918
+ }
919
+
920
+ initialize_worktree_mem0_layer() {
921
+ local worktree="$1"
922
+ local branch="$2"
923
+ local base_branch="$3"
924
+ local task_slug="$4"
925
+ local agent_slug="$5"
926
+
927
+ local omx_dir="${worktree}/.omx"
928
+ local mem0_dir="${omx_dir}/mem0"
929
+ local notepad_path="${mem0_dir}/notepad.md"
930
+ local project_memory_path="${mem0_dir}/project-memory.json"
931
+ local scope_path="${mem0_dir}/worktree-scope.json"
932
+ local created_at
933
+ local now
934
+
935
+ mkdir -p "$mem0_dir"
936
+
937
+ if [[ ! -f "$notepad_path" ]]; then
938
+ cat >"$notepad_path" <<EOF
939
+ # Mem0 Worktree Memory
940
+
941
+ - Scope: worktree
942
+ - Branch: ${branch}
943
+ - Base: ${base_branch}
944
+ - Task: ${task_slug}
945
+ - Agent: ${agent_slug}
946
+
947
+ ## WORKING MEMORY
948
+ EOF
949
+ fi
950
+
951
+ if [[ ! -f "$project_memory_path" ]]; then
952
+ created_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
953
+ cat >"$project_memory_path" <<EOF
954
+ {
955
+ "version": 1,
956
+ "scope": "worktree",
957
+ "branch": "$(json_escape "$branch")",
958
+ "baseBranch": "$(json_escape "$base_branch")",
959
+ "taskSlug": "$(json_escape "$task_slug")",
960
+ "agentSlug": "$(json_escape "$agent_slug")",
961
+ "createdAt": "$(json_escape "$created_at")",
962
+ "notes": [],
963
+ "directives": []
964
+ }
965
+ EOF
966
+ fi
967
+
968
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
969
+ cat >"$scope_path" <<EOF
970
+ {
971
+ "version": 1,
972
+ "scope": "worktree",
973
+ "branch": "$(json_escape "$branch")",
974
+ "baseBranch": "$(json_escape "$base_branch")",
975
+ "taskSlug": "$(json_escape "$task_slug")",
976
+ "agentSlug": "$(json_escape "$agent_slug")",
977
+ "worktreePath": "$(json_escape "$worktree")",
978
+ "updatedAt": "$(json_escape "$now")",
979
+ "notepadPath": ".omx/mem0/notepad.md",
980
+ "projectMemoryPath": ".omx/mem0/project-memory.json"
981
+ }
982
+ EOF
983
+
984
+ link_worktree_mem0_compat_file "$notepad_path" "${omx_dir}/notepad.md" "mem0/notepad.md"
985
+ link_worktree_mem0_compat_file "$project_memory_path" "${omx_dir}/project-memory.json" "mem0/project-memory.json"
986
+ echo "[agent-branch-start] Mem0 worktree memory: ${mem0_dir}"
987
+ }
988
+
989
+ run_startup_context_artifacts() {
990
+ local repo="$1"
991
+ local worktree="$2"
992
+ local branch="$3"
993
+ local base_branch="$4"
994
+ local pr_ref="$5"
995
+ local repo_ref="$6"
996
+ local branch_slug
997
+ local github_context_dir="${worktree}/.omx/context/github"
998
+ local merge_gate_dir="${worktree}/.omx/state/merge-gates"
999
+ local context_pack_dir="${worktree}/.omx/context/packs"
1000
+ local context_json=""
1001
+ local conflict_json=""
1002
+ local context_pack_json=""
1003
+ local conflict_passed=1
1004
+
1005
+ mkdir -p "$github_context_dir" "$merge_gate_dir" "$context_pack_dir"
1006
+ branch_slug="$(sanitize_slug "${branch//\//-}" "context-pack")"
1007
+
1008
+ if [[ "$GH_SYNC_ON_START" -eq 1 ]]; then
1009
+ if [[ -x "${repo}/scripts/omx-gh-sync.sh" ]]; then
1010
+ local sync_args=(--branch "$branch" --output-dir "$github_context_dir")
1011
+ if [[ -n "$pr_ref" ]]; then
1012
+ sync_args+=(--pr "$pr_ref")
1013
+ fi
1014
+ if [[ -n "$repo_ref" ]]; then
1015
+ sync_args+=(--repo "$repo_ref")
1016
+ fi
1017
+ local sync_output=""
1018
+ if sync_output="$(bash "${repo}/scripts/omx-gh-sync.sh" "${sync_args[@]}" 2>&1)"; then
1019
+ printf '%s\n' "$sync_output"
1020
+ context_json="$(printf '%s\n' "$sync_output" | sed -n 's/^Context JSON: //p' | tail -n1)"
1021
+ else
1022
+ echo "[agent-branch-start] Warning: GitHub context sync failed; continuing with local-only startup context." >&2
1023
+ printf '%s\n' "$sync_output" >&2
1024
+ fi
1025
+ else
1026
+ echo "[agent-branch-start] Warning: scripts/omx-gh-sync.sh is missing; skipping GitHub context sync." >&2
1027
+ fi
1028
+ else
1029
+ echo "[agent-branch-start] GitHub context sync disabled (--no-gh-sync)."
1030
+ fi
1031
+
1032
+ if [[ -x "${repo}/scripts/agent-conflict-predict.sh" ]]; then
1033
+ local conflict_output=""
1034
+ local conflict_args=(--branch "$branch" --base "$base_branch" --output-dir "$merge_gate_dir")
1035
+ if conflict_output="$(bash "${repo}/scripts/agent-conflict-predict.sh" "${conflict_args[@]}" 2>&1)"; then
1036
+ printf '%s\n' "$conflict_output"
1037
+ conflict_json="$(printf '%s\n' "$conflict_output" | sed -n 's/^Conflict JSON: //p' | tail -n1)"
1038
+ else
1039
+ conflict_passed=0
1040
+ echo "[agent-branch-start] Warning: conflict predictor reported overlaps/locks before coding begins." >&2
1041
+ printf '%s\n' "$conflict_output" >&2
1042
+ fi
1043
+ fi
1044
+
1045
+ if [[ -x "${repo}/scripts/omx-context-pack.sh" ]]; then
1046
+ local pack_args=(
1047
+ --slug "$branch_slug"
1048
+ --branch "$branch"
1049
+ --base "$base_branch"
1050
+ --output-dir "$context_pack_dir"
1051
+ )
1052
+ if [[ -n "$context_json" ]]; then
1053
+ pack_args+=(--context-file "$context_json")
1054
+ fi
1055
+ if [[ -n "$conflict_json" ]]; then
1056
+ pack_args+=(--conflict-file "$conflict_json")
1057
+ fi
1058
+ local pack_output=""
1059
+ if pack_output="$(bash "${repo}/scripts/omx-context-pack.sh" "${pack_args[@]}" 2>&1)"; then
1060
+ printf '%s\n' "$pack_output"
1061
+ context_pack_json="$(printf '%s\n' "$pack_output" | sed -n 's/^Context pack JSON: //p' | tail -n1)"
1062
+ else
1063
+ echo "[agent-branch-start] Warning: context pack assembly failed; continuing without startup pack." >&2
1064
+ printf '%s\n' "$pack_output" >&2
1065
+ fi
1066
+ fi
1067
+
1068
+ python3 - "${github_context_dir}/sandbox-startup-latest.json" "$branch" "$base_branch" "$pr_ref" "$repo_ref" "$GH_SYNC_ON_START" "$conflict_passed" "$context_json" "$conflict_json" "$context_pack_json" <<'PY'
1069
+ from __future__ import annotations
1070
+
1071
+ import json
1072
+ import sys
1073
+ from datetime import datetime, timezone
1074
+ from pathlib import Path
1075
+
1076
+ (
1077
+ _,
1078
+ output_path,
1079
+ branch_name,
1080
+ base_name,
1081
+ pr_value,
1082
+ repo_value,
1083
+ gh_sync_value,
1084
+ conflict_value,
1085
+ context_json_path,
1086
+ conflict_json_path,
1087
+ context_pack_path,
1088
+ ) = sys.argv
1089
+
1090
+ payload = {
1091
+ "version": 1,
1092
+ "generated_at": datetime.now(timezone.utc).isoformat(),
1093
+ "branch": branch_name,
1094
+ "base_branch": base_name,
1095
+ "pr": pr_value,
1096
+ "repo": repo_value,
1097
+ "gh_sync_enabled": int(gh_sync_value),
1098
+ "conflict_passed": int(conflict_value),
1099
+ "context_json": context_json_path,
1100
+ "conflict_json": conflict_json_path,
1101
+ "context_pack_json": context_pack_path,
1102
+ }
1103
+
1104
+ target = Path(output_path)
1105
+ target.parent.mkdir(parents=True, exist_ok=True)
1106
+ target.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1107
+ PY
1108
+
1109
+ echo "[agent-branch-start] Startup metadata: ${github_context_dir}/sandbox-startup-latest.json"
1110
+ }
1111
+
312
1112
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
313
1113
  echo "[agent-branch-start] Not inside a git repository." >&2
314
1114
  exit 1
@@ -324,20 +1124,39 @@ fi
324
1124
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
325
1125
  current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
326
1126
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
1127
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
327
1128
  if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
328
1129
  BASE_BRANCH="$current_branch"
1130
+ elif [[ -n "$current_branch" && "$current_branch" == agent/* ]]; then
1131
+ BASE_BRANCH="$current_branch"
1132
+ echo "[agent-branch-start] Using current agent branch '${BASE_BRANCH}' as helper base."
1133
+ elif [[ -n "$configured_base" ]]; then
1134
+ BASE_BRANCH="$configured_base"
329
1135
  else
330
- configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
331
- if [[ -n "$configured_base" ]]; then
332
- BASE_BRANCH="$configured_base"
333
- elif [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
1136
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
334
1137
  BASE_BRANCH="$current_branch"
335
1138
  else
336
- BASE_BRANCH="dev"
1139
+ BASE_BRANCH="$(resolve_default_base_branch_for_agent_subbranch "$repo_root" "$protected_branches_raw" || printf 'dev')"
337
1140
  fi
338
1141
  fi
339
1142
  fi
340
1143
 
1144
+ helper_branch_assist_mode=0
1145
+ if is_helper_agent_base_branch "$BASE_BRANCH"; then
1146
+ helper_branch_assist_mode=1
1147
+ OPENSPEC_AUTO_INIT=0
1148
+ OPENSPEC_CHANGE_INIT=0
1149
+ OPENSPEC_PLAN_INIT=0
1150
+ OPENSPEC_CHANGE_MINIMAL=0
1151
+ echo "[agent-branch-start] Helper branch base '${BASE_BRANCH}' detected; skipping OpenSpec auto-init for joined-agent assist."
1152
+ elif [[ "$TIER_LEVEL_EXPLICIT" -eq 1 && "$TIER_LEVEL" == "T0" ]]; then
1153
+ echo "[agent-branch-start] Tier T0: skipping OpenSpec change + plan workspace scaffolding."
1154
+ elif [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
1155
+ echo "[agent-branch-start] OpenSpec auto-init is mandatory for non-helper agent branches; ignoring disabled override." >&2
1156
+ OPENSPEC_AUTO_INIT=1
1157
+ OPENSPEC_CHANGE_INIT=1
1158
+ fi
1159
+
341
1160
  if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
342
1161
  git fetch origin "${BASE_BRANCH}" --quiet
343
1162
  start_ref="origin/${BASE_BRANCH}"
@@ -350,74 +1169,74 @@ else
350
1169
  fi
351
1170
 
352
1171
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
353
- agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
1172
+ agent_type="$(normalize_agent_type "$AGENT_NAME")"
354
1173
  snapshot_name="$(resolve_active_codex_snapshot_name)"
355
- snapshot_slug="$(sanitize_slug "$snapshot_name" "")"
1174
+ nickname="$(extract_nickname "$snapshot_name")"
1175
+ agent_slug="${agent_type}-${nickname}"
1176
+ branch_descriptor="$(compose_branch_descriptor "$task_slug")"
356
1177
  timestamp="$(date +%Y%m%d-%H%M%S)"
357
- if [[ -n "$snapshot_slug" ]]; then
358
- branch_name_base="agent/${agent_slug}/${snapshot_slug}-${task_slug}"
359
- else
360
- branch_name_base="agent/${agent_slug}/${task_slug}"
1178
+ branch_name_base="agent/${agent_slug}/${branch_descriptor}"
1179
+
1180
+ if [[ "$PRINT_NAME_ONLY" -eq 1 ]]; then
1181
+ printf '%s\n' "$branch_name_base"
1182
+ exit 0
361
1183
  fi
362
1184
 
363
1185
  branch_name="$branch_name_base"
364
- branch_suffix=2
365
- while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
366
- branch_name="${branch_name_base}-${branch_suffix}"
367
- branch_suffix=$((branch_suffix + 1))
368
- done
369
-
370
1186
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
371
1187
  mkdir -p "$worktree_root"
372
- worktree_path="${worktree_root}/${branch_name//\//__}"
1188
+ worktree_path=""
1189
+ reused_existing_worktree=0
1190
+
1191
+ if git show-ref --verify --quiet "refs/heads/${branch_name_base}"; then
1192
+ existing_worktree_path="$(get_worktree_for_branch "$branch_name_base" || true)"
1193
+ if [[ -n "$existing_worktree_path" && -d "$existing_worktree_path" ]] \
1194
+ && git -C "$repo_root" merge-base --is-ancestor "$branch_name_base" "$start_ref" >/dev/null 2>&1; then
1195
+ if is_clean_worktree "$existing_worktree_path" || worktree_matches_bootstrap_manifest "$existing_worktree_path"; then
1196
+ worktree_path="$existing_worktree_path"
1197
+ reused_existing_worktree=1
1198
+ echo "[agent-branch-start] Reusing untouched sandbox branch/worktree: ${branch_name_base} (${worktree_path})"
1199
+ fi
1200
+ fi
1201
+ fi
1202
+
1203
+ if [[ "$reused_existing_worktree" -eq 0 ]]; then
1204
+ branch_suffix=2
1205
+ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
1206
+ branch_name="${branch_name_base}-${branch_suffix}"
1207
+ branch_suffix=$((branch_suffix + 1))
1208
+ done
1209
+ worktree_path="${worktree_root}/${branch_name//\//__}"
1210
+ if [[ -e "$worktree_path" ]]; then
1211
+ echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
1212
+ exit 1
1213
+ fi
1214
+ fi
1215
+
373
1216
  openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
374
1217
  openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
375
1218
  openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
376
1219
 
377
- if [[ -e "$worktree_path" ]]; then
378
- echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
379
- exit 1
380
- fi
381
-
382
- auto_transfer_stash_ref=""
383
- auto_transfer_message=""
384
- auto_transfer_source_branch=""
385
- current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
1220
+ primary_branch_before="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
386
1221
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
387
- if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
1222
+ should_migrate_protected_changes=0
1223
+ if [[ -n "$primary_branch_before" && "$primary_branch_before" != "HEAD" ]] && is_protected_branch_name "$primary_branch_before" "$protected_branches_raw"; then
388
1224
  if has_local_changes "$repo_root"; then
389
- auto_transfer_message="musafety-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
390
- if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
391
- auto_transfer_stash_ref="$(
392
- git -C "$repo_root" stash list \
393
- | awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
394
- )"
395
- if [[ -n "$auto_transfer_stash_ref" ]]; then
396
- auto_transfer_source_branch="$current_branch"
397
- echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
398
- fi
1225
+ if [[ "$MIGRATE_PROTECTED_CHANGES" -eq 1 ]]; then
1226
+ should_migrate_protected_changes=1
1227
+ echo "[agent-branch-start] Detected local changes on protected branch '${primary_branch_before}'. Scheduling migration into sandbox worktree."
1228
+ else
1229
+ echo "[agent-branch-start] Detected local changes on protected branch '${primary_branch_before}'. Leaving them in place (GUARDEX_MIGRATE_PROTECTED_CHANGES=0)."
399
1230
  fi
400
1231
  fi
401
1232
  fi
402
1233
 
403
- git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref"
404
- git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
1234
+ if [[ "$reused_existing_worktree" -eq 0 ]]; then
1235
+ git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref"
1236
+ git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
405
1237
 
406
- if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
407
- git -C "$worktree_path" branch --set-upstream-to="origin/${BASE_BRANCH}" "$branch_name" >/dev/null 2>&1 || true
408
- fi
409
-
410
- if [[ -n "$auto_transfer_stash_ref" ]]; then
411
- if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
412
- git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
413
- transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
414
- echo "[agent-branch-start] Moved local changes from '${transfer_label}' into '${branch_name}'."
415
- else
416
- echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
417
- transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
418
- echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${transfer_label}." >&2
419
- echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
420
- exit 1
1238
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
1239
+ git -C "$worktree_path" branch --set-upstream-to="origin/${BASE_BRANCH}" "$branch_name" >/dev/null 2>&1 || true
421
1240
  fi
422
1241
  fi
423
1242
 
@@ -425,19 +1244,61 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag
425
1244
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
426
1245
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
427
1246
  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
1247
+ hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" ".venv"
1248
+ if [[ "$reused_existing_worktree" -eq 0 ]]; then
1249
+ if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug" "$branch_name"; then
1250
+ exit 1
1251
+ fi
1252
+ if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
1253
+ exit 1
1254
+ fi
430
1255
  fi
431
- if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
432
- exit 1
1256
+ initialize_worktree_mem0_layer "$worktree_path" "$branch_name" "$BASE_BRANCH" "$task_slug" "$agent_slug"
1257
+
1258
+ if [[ "$should_migrate_protected_changes" -eq 1 ]]; then
1259
+ if ! migrate_local_changes_to_worktree "$repo_root" "$primary_branch_before" "$branch_name" "$worktree_path"; then
1260
+ echo "[agent-branch-start] Warning: automatic protected-branch change migration did not fully complete." >&2
1261
+ fi
1262
+ fi
1263
+
1264
+ run_startup_context_artifacts "$repo_root" "$worktree_path" "$branch_name" "$BASE_BRANCH" "$PR_REF" "$GH_REPO_REF"
1265
+ record_worktree_bootstrap_manifest "$worktree_path" "$branch_name" "$BASE_BRANCH" "$openspec_change_slug" "$openspec_plan_slug" "$TIER_LEVEL"
1266
+
1267
+ primary_branch_after="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
1268
+ if [[ -n "$primary_branch_before" && "$primary_branch_before" != "HEAD" && "$primary_branch_after" != "$primary_branch_before" ]]; then
1269
+ echo "[agent-branch-start] Warning: primary checkout moved from '${primary_branch_before}' to '${primary_branch_after}'. Restoring '${primary_branch_before}'."
1270
+ git -C "$repo_root" checkout -q "$primary_branch_before"
1271
+ primary_branch_after="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
1272
+ if [[ "$primary_branch_after" != "$primary_branch_before" ]]; then
1273
+ echo "[agent-branch-start] Failed to restore primary checkout branch '${primary_branch_before}'." >&2
1274
+ exit 1
1275
+ fi
433
1276
  fi
434
1277
 
435
1278
  echo "[agent-branch-start] Created branch: ${branch_name}"
436
1279
  echo "[agent-branch-start] Worktree: ${worktree_path}"
437
- echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
438
- echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
1280
+ echo "[agent-branch-start] Tier: ${TIER_LEVEL}"
1281
+ if [[ "$helper_branch_assist_mode" -eq 1 ]]; then
1282
+ echo "[agent-branch-start] OpenSpec change: skipped (helper branch assisting ${BASE_BRANCH})"
1283
+ echo "[agent-branch-start] OpenSpec plan: skipped (helper branch assisting ${BASE_BRANCH})"
1284
+ else
1285
+ if [[ "$OPENSPEC_CHANGE_INIT" -eq 1 ]]; then
1286
+ if [[ "$OPENSPEC_CHANGE_MINIMAL" -eq 1 ]]; then
1287
+ echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug} (minimal / notes-only)"
1288
+ else
1289
+ echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
1290
+ fi
1291
+ else
1292
+ echo "[agent-branch-start] OpenSpec change: skipped (tier ${TIER_LEVEL})"
1293
+ fi
1294
+ if [[ "$OPENSPEC_PLAN_INIT" -eq 1 ]]; then
1295
+ echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
1296
+ else
1297
+ echo "[agent-branch-start] OpenSpec plan: skipped (tier ${TIER_LEVEL})"
1298
+ fi
1299
+ fi
439
1300
  echo "[agent-branch-start] Next steps:"
440
1301
  echo " cd \"${worktree_path}\""
441
1302
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
442
1303
  echo " # implement + commit"
443
- echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base dev --via-pr --wait-for-merge"
1304
+ echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup"