@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.
- package/bin/multiagent-safety.js +21 -5
- package/package.json +3 -1
- package/templates/AGENTS.multiagent-safety.md +1 -0
- package/templates/githooks/post-checkout +68 -0
- package/templates/githooks/post-merge +3 -39
- package/templates/githooks/pre-commit +193 -27
- package/templates/githooks/pre-push +0 -0
- package/templates/scripts/agent-branch-finish.sh +702 -70
- package/templates/scripts/agent-branch-start.sh +877 -76
- package/templates/scripts/agent-worktree-prune.sh +353 -65
- package/templates/scripts/codex-agent.sh +238 -626
- package/templates/scripts/install-agent-git-hooks.sh +27 -4
- package/templates/scripts/openspec/init-change-workspace.sh +50 -4
- package/templates/scripts/openspec/init-plan-workspace.sh +495 -48
- package/templates/scripts/review-bot-watch.sh +11 -11
|
@@ -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:-
|
|
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
|
|
135
|
-
local
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
464
|
-
git -C "$repo_root"
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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]
|
|
498
|
-
|
|
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
|
|
1304
|
+
echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge --cleanup"
|