@imdeadpool/guardex 6.0.0 → 6.1.0

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="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
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}"
10
14
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
11
15
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
12
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
 
@@ -131,23 +163,124 @@ checksum_slug_suffix() {
131
163
  }
132
164
 
133
165
  compose_branch_descriptor() {
134
- local snapshot_slug="$1"
135
- local task_slug="$2"
136
- local snapshot_max task_max task_part snapshot_part checksum_input checksum_part
137
- snapshot_max="$(normalize_positive_int "${GUARDEX_BRANCH_SNAPSHOT_SLUG_MAX:-18}" "18")"
166
+ local task_slug="$1"
167
+ local task_max task_part checksum_part
138
168
  task_max="$(normalize_positive_int "${GUARDEX_BRANCH_TASK_SLUG_MAX:-36}" "36")"
139
169
  task_part="$(shorten_slug "$task_slug" "$task_max")"
140
- if [[ -n "$snapshot_slug" ]]; then
141
- snapshot_part="$(shorten_slug "$snapshot_slug" "$snapshot_max")"
142
- checksum_input="${snapshot_slug}--${task_slug}"
143
- checksum_part="$(checksum_slug_suffix "$checksum_input")"
144
- printf '%s-%s-%s' "$snapshot_part" "$task_part" "$checksum_part"
145
- return 0
146
- fi
147
170
  checksum_part="$(checksum_slug_suffix "$task_slug")"
148
171
  printf '%s-%s' "$task_part" "$checksum_part"
149
172
  }
150
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
+
151
284
  normalize_bool() {
152
285
  local raw="${1:-}"
153
286
  local fallback="${2:-0}"
@@ -162,6 +295,63 @@ normalize_bool() {
162
295
  }
163
296
 
164
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
+ }
165
355
 
166
356
  resolve_openspec_plan_slug() {
167
357
  local branch_name="$1"
@@ -224,6 +414,54 @@ has_local_changes() {
224
414
  return 1
225
415
  }
226
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
+
227
465
  resolve_protected_branches() {
228
466
  local root="$1"
229
467
  local raw
@@ -246,6 +484,51 @@ is_protected_branch_name() {
246
484
  return 1
247
485
  }
248
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
+
249
532
  hydrate_local_helper_in_worktree() {
250
533
  local repo="$1"
251
534
  local worktree="$2"
@@ -303,7 +586,7 @@ initialize_openspec_plan_workspace() {
303
586
 
304
587
  hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
305
588
 
306
- if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
589
+ if [[ "${OPENSPEC_PLAN_INIT:-$OPENSPEC_AUTO_INIT}" -ne 1 ]]; then
307
590
  return 0
308
591
  fi
309
592
 
@@ -338,6 +621,7 @@ initialize_openspec_change_workspace() {
338
621
  local worktree="$2"
339
622
  local change_slug="$3"
340
623
  local capability_slug="$4"
624
+ local branch_name="${5:-}"
341
625
 
342
626
  hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh"
343
627
 
@@ -358,7 +642,8 @@ initialize_openspec_change_workspace() {
358
642
  local init_output=""
359
643
  if ! init_output="$(
360
644
  cd "$worktree"
361
- 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
362
647
  )"; then
363
648
  printf '%s\n' "$init_output" >&2
364
649
  echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
@@ -371,6 +656,459 @@ initialize_openspec_change_workspace() {
371
656
  echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}"
372
657
  }
373
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
+
374
1112
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
375
1113
  echo "[agent-branch-start] Not inside a git repository." >&2
376
1114
  exit 1
@@ -386,20 +1124,39 @@ fi
386
1124
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
387
1125
  current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
388
1126
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
1127
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
389
1128
  if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
390
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"
391
1135
  else
392
- configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
393
- if [[ -n "$configured_base" ]]; then
394
- BASE_BRANCH="$configured_base"
395
- elif [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
1136
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
396
1137
  BASE_BRANCH="$current_branch"
397
1138
  else
398
- BASE_BRANCH="dev"
1139
+ BASE_BRANCH="$(resolve_default_base_branch_for_agent_subbranch "$repo_root" "$protected_branches_raw" || printf 'dev')"
399
1140
  fi
400
1141
  fi
401
1142
  fi
402
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
+
403
1160
  if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
404
1161
  git fetch origin "${BASE_BRANCH}" --quiet
405
1162
  start_ref="origin/${BASE_BRANCH}"
@@ -412,72 +1169,74 @@ else
412
1169
  fi
413
1170
 
414
1171
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
415
- agent_slug_raw="$(sanitize_slug "$AGENT_NAME" "agent")"
416
- agent_slug="$(shorten_slug "$agent_slug_raw" "${GUARDEX_BRANCH_AGENT_SLUG_MAX:-24}")"
1172
+ agent_type="$(normalize_agent_type "$AGENT_NAME")"
417
1173
  snapshot_name="$(resolve_active_codex_snapshot_name)"
418
- snapshot_slug="$(sanitize_optional_slug "$snapshot_name" "snapshot")"
419
- branch_descriptor="$(compose_branch_descriptor "$snapshot_slug" "$task_slug")"
1174
+ nickname="$(extract_nickname "$snapshot_name")"
1175
+ agent_slug="${agent_type}-${nickname}"
1176
+ branch_descriptor="$(compose_branch_descriptor "$task_slug")"
420
1177
  timestamp="$(date +%Y%m%d-%H%M%S)"
421
1178
  branch_name_base="agent/${agent_slug}/${branch_descriptor}"
422
1179
 
423
- branch_name="$branch_name_base"
424
- branch_suffix=2
425
- while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
426
- branch_name="${branch_name_base}-${branch_suffix}"
427
- branch_suffix=$((branch_suffix + 1))
428
- done
1180
+ if [[ "$PRINT_NAME_ONLY" -eq 1 ]]; then
1181
+ printf '%s\n' "$branch_name_base"
1182
+ exit 0
1183
+ fi
429
1184
 
1185
+ branch_name="$branch_name_base"
430
1186
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
431
1187
  mkdir -p "$worktree_root"
432
- 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
+
433
1216
  openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
434
1217
  openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
435
1218
  openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
436
1219
 
437
- if [[ -e "$worktree_path" ]]; then
438
- echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
439
- exit 1
440
- fi
441
-
442
- auto_transfer_stash_ref=""
443
- auto_transfer_message=""
444
- auto_transfer_source_branch=""
445
- 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)"
446
1221
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
447
- 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
448
1224
  if has_local_changes "$repo_root"; then
449
- auto_transfer_message="guardex-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
450
- if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
451
- auto_transfer_stash_ref="$(
452
- git -C "$repo_root" stash list \
453
- | awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
454
- )"
455
- if [[ -n "$auto_transfer_stash_ref" ]]; then
456
- auto_transfer_source_branch="$current_branch"
457
- echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
458
- 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)."
459
1230
  fi
460
1231
  fi
461
1232
  fi
462
1233
 
463
- git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref"
464
- git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$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
465
1237
 
466
- if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
467
- git -C "$worktree_path" branch --set-upstream-to="origin/${BASE_BRANCH}" "$branch_name" >/dev/null 2>&1 || true
468
- fi
469
-
470
- if [[ -n "$auto_transfer_stash_ref" ]]; then
471
- if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
472
- git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
473
- transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
474
- echo "[agent-branch-start] Moved local changes from '${transfer_label}' into '${branch_name}'."
475
- else
476
- echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
477
- transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
478
- echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${transfer_label}." >&2
479
- echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
480
- 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
481
1240
  fi
482
1241
  fi
483
1242
 
@@ -485,19 +1244,61 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag
485
1244
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
486
1245
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
487
1246
  hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
488
- if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug"; then
489
- 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
490
1255
  fi
491
- if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
492
- 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
493
1276
  fi
494
1277
 
495
1278
  echo "[agent-branch-start] Created branch: ${branch_name}"
496
1279
  echo "[agent-branch-start] Worktree: ${worktree_path}"
497
- echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
498
- 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
499
1300
  echo "[agent-branch-start] Next steps:"
500
1301
  echo " cd \"${worktree_path}\""
501
1302
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
502
1303
  echo " # implement + commit"
503
- 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"