@imdeadpool/guardex 6.1.0 → 7.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.
- package/README.md +63 -39
- package/bin/multiagent-safety.js +372 -296
- package/package.json +3 -5
- package/templates/AGENTS.multiagent-safety.md +9 -82
- package/templates/claude/commands/guardex.md +6 -12
- package/templates/codex/skills/guardex/SKILL.md +18 -64
- package/templates/githooks/post-merge +39 -3
- package/templates/githooks/pre-commit +27 -193
- package/templates/githooks/pre-push +0 -0
- package/templates/scripts/agent-branch-finish.sh +70 -702
- package/templates/scripts/agent-branch-start.sh +76 -877
- package/templates/scripts/agent-worktree-prune.sh +65 -353
- package/templates/scripts/codex-agent.sh +626 -238
- package/templates/scripts/install-agent-git-hooks.sh +4 -27
- package/templates/scripts/openspec/init-change-workspace.sh +4 -50
- package/templates/scripts/openspec/init-plan-workspace.sh +48 -495
- package/templates/scripts/review-bot-watch.sh +11 -11
|
@@ -6,17 +6,10 @@ 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="${
|
|
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}"
|
|
9
|
+
OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
|
|
14
10
|
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
|
|
15
11
|
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
|
|
16
12
|
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
|
|
20
13
|
POSITIONAL_ARGS=()
|
|
21
14
|
|
|
22
15
|
while [[ $# -gt 0 ]]; do
|
|
@@ -43,31 +36,6 @@ while [[ $# -gt 0 ]]; do
|
|
|
43
36
|
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
|
|
44
37
|
shift 2
|
|
45
38
|
;;
|
|
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
|
-
;;
|
|
71
39
|
--)
|
|
72
40
|
shift
|
|
73
41
|
while [[ $# -gt 0 ]]; do
|
|
@@ -78,7 +46,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
78
46
|
;;
|
|
79
47
|
-*)
|
|
80
48
|
echo "[agent-branch-start] Unknown option: $1" >&2
|
|
81
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
49
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
82
50
|
exit 1
|
|
83
51
|
;;
|
|
84
52
|
*)
|
|
@@ -90,7 +58,7 @@ done
|
|
|
90
58
|
|
|
91
59
|
if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
|
|
92
60
|
echo "[agent-branch-start] Too many positional arguments." >&2
|
|
93
|
-
echo "Usage: $0 [task] [agent] [base] [--
|
|
61
|
+
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
|
|
94
62
|
exit 1
|
|
95
63
|
fi
|
|
96
64
|
|
|
@@ -163,122 +131,21 @@ checksum_slug_suffix() {
|
|
|
163
131
|
}
|
|
164
132
|
|
|
165
133
|
compose_branch_descriptor() {
|
|
166
|
-
local
|
|
167
|
-
local
|
|
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")"
|
|
168
138
|
task_max="$(normalize_positive_int "${GUARDEX_BRANCH_TASK_SLUG_MAX:-36}" "36")"
|
|
169
139
|
task_part="$(shorten_slug "$task_slug" "$task_max")"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
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"
|
|
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"
|
|
268
145
|
return 0
|
|
269
146
|
fi
|
|
270
|
-
|
|
271
|
-
|
|
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"
|
|
147
|
+
checksum_part="$(checksum_slug_suffix "$task_slug")"
|
|
148
|
+
printf '%s-%s' "$task_part" "$checksum_part"
|
|
282
149
|
}
|
|
283
150
|
|
|
284
151
|
normalize_bool() {
|
|
@@ -295,63 +162,6 @@ normalize_bool() {
|
|
|
295
162
|
}
|
|
296
163
|
|
|
297
164
|
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
|
-
}
|
|
355
165
|
|
|
356
166
|
resolve_openspec_plan_slug() {
|
|
357
167
|
local branch_name="$1"
|
|
@@ -414,54 +224,6 @@ has_local_changes() {
|
|
|
414
224
|
return 1
|
|
415
225
|
}
|
|
416
226
|
|
|
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
|
-
|
|
465
227
|
resolve_protected_branches() {
|
|
466
228
|
local root="$1"
|
|
467
229
|
local raw
|
|
@@ -484,51 +246,6 @@ is_protected_branch_name() {
|
|
|
484
246
|
return 1
|
|
485
247
|
}
|
|
486
248
|
|
|
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
|
-
|
|
532
249
|
hydrate_local_helper_in_worktree() {
|
|
533
250
|
local repo="$1"
|
|
534
251
|
local worktree="$2"
|
|
@@ -586,7 +303,7 @@ initialize_openspec_plan_workspace() {
|
|
|
586
303
|
|
|
587
304
|
hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
|
|
588
305
|
|
|
589
|
-
if [[ "$
|
|
306
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
|
|
590
307
|
return 0
|
|
591
308
|
fi
|
|
592
309
|
|
|
@@ -621,7 +338,6 @@ initialize_openspec_change_workspace() {
|
|
|
621
338
|
local worktree="$2"
|
|
622
339
|
local change_slug="$3"
|
|
623
340
|
local capability_slug="$4"
|
|
624
|
-
local branch_name="${5:-}"
|
|
625
341
|
|
|
626
342
|
hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-change-workspace.sh"
|
|
627
343
|
|
|
@@ -642,8 +358,7 @@ initialize_openspec_change_workspace() {
|
|
|
642
358
|
local init_output=""
|
|
643
359
|
if ! init_output="$(
|
|
644
360
|
cd "$worktree"
|
|
645
|
-
|
|
646
|
-
bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" "$branch_name" 2>&1
|
|
361
|
+
bash "scripts/openspec/init-change-workspace.sh" "$change_slug" "$capability_slug" 2>&1
|
|
647
362
|
)"; then
|
|
648
363
|
printf '%s\n' "$init_output" >&2
|
|
649
364
|
echo "[agent-branch-start] OpenSpec workspace initialization failed for change '${change_slug}'." >&2
|
|
@@ -656,459 +371,6 @@ initialize_openspec_change_workspace() {
|
|
|
656
371
|
echo "[agent-branch-start] OpenSpec change workspace: ${worktree}/openspec/changes/${change_slug}"
|
|
657
372
|
}
|
|
658
373
|
|
|
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
|
-
|
|
1112
374
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
1113
375
|
echo "[agent-branch-start] Not inside a git repository." >&2
|
|
1114
376
|
exit 1
|
|
@@ -1124,39 +386,20 @@ fi
|
|
|
1124
386
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
1125
387
|
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
1126
388
|
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
1127
|
-
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
1128
389
|
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
1129
390
|
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"
|
|
1135
391
|
else
|
|
1136
|
-
|
|
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
|
|
1137
396
|
BASE_BRANCH="$current_branch"
|
|
1138
397
|
else
|
|
1139
|
-
BASE_BRANCH="
|
|
398
|
+
BASE_BRANCH="dev"
|
|
1140
399
|
fi
|
|
1141
400
|
fi
|
|
1142
401
|
fi
|
|
1143
402
|
|
|
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
|
-
|
|
1160
403
|
if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
1161
404
|
git fetch origin "${BASE_BRANCH}" --quiet
|
|
1162
405
|
start_ref="origin/${BASE_BRANCH}"
|
|
@@ -1169,74 +412,72 @@ else
|
|
|
1169
412
|
fi
|
|
1170
413
|
|
|
1171
414
|
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
1172
|
-
|
|
415
|
+
agent_slug_raw="$(sanitize_slug "$AGENT_NAME" "agent")"
|
|
416
|
+
agent_slug="$(shorten_slug "$agent_slug_raw" "${GUARDEX_BRANCH_AGENT_SLUG_MAX:-24}")"
|
|
1173
417
|
snapshot_name="$(resolve_active_codex_snapshot_name)"
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
branch_descriptor="$(compose_branch_descriptor "$task_slug")"
|
|
418
|
+
snapshot_slug="$(sanitize_optional_slug "$snapshot_name" "snapshot")"
|
|
419
|
+
branch_descriptor="$(compose_branch_descriptor "$snapshot_slug" "$task_slug")"
|
|
1177
420
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
|
1178
421
|
branch_name_base="agent/${agent_slug}/${branch_descriptor}"
|
|
1179
422
|
|
|
1180
|
-
if [[ "$PRINT_NAME_ONLY" -eq 1 ]]; then
|
|
1181
|
-
printf '%s\n' "$branch_name_base"
|
|
1182
|
-
exit 0
|
|
1183
|
-
fi
|
|
1184
|
-
|
|
1185
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
|
|
429
|
+
|
|
1186
430
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
1187
431
|
mkdir -p "$worktree_root"
|
|
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
|
-
|
|
432
|
+
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
1216
433
|
openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
|
|
1217
434
|
openspec_change_slug="$(resolve_openspec_change_slug "$branch_name" "$task_slug")"
|
|
1218
435
|
openspec_capability_slug="$(resolve_openspec_capability_slug "$task_slug")"
|
|
1219
436
|
|
|
1220
|
-
|
|
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)"
|
|
1221
446
|
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
1222
|
-
|
|
1223
|
-
if [[ -n "$primary_branch_before" && "$primary_branch_before" != "HEAD" ]] && is_protected_branch_name "$primary_branch_before" "$protected_branches_raw"; then
|
|
447
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
1224
448
|
if has_local_changes "$repo_root"; then
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
|
1230
459
|
fi
|
|
1231
460
|
fi
|
|
1232
461
|
fi
|
|
1233
462
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
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
|
|
1237
465
|
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1240
481
|
fi
|
|
1241
482
|
fi
|
|
1242
483
|
|
|
@@ -1244,61 +485,19 @@ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-ag
|
|
|
1244
485
|
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
|
|
1245
486
|
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
|
|
1246
487
|
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
|
1255
|
-
fi
|
|
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
|
|
488
|
+
if ! initialize_openspec_change_workspace "$repo_root" "$worktree_path" "$openspec_change_slug" "$openspec_capability_slug"; then
|
|
489
|
+
exit 1
|
|
1262
490
|
fi
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
|
491
|
+
if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
|
|
492
|
+
exit 1
|
|
1276
493
|
fi
|
|
1277
494
|
|
|
1278
495
|
echo "[agent-branch-start] Created branch: ${branch_name}"
|
|
1279
496
|
echo "[agent-branch-start] Worktree: ${worktree_path}"
|
|
1280
|
-
echo "[agent-branch-start]
|
|
1281
|
-
|
|
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
|
|
497
|
+
echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
|
|
498
|
+
echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
|
|
1300
499
|
echo "[agent-branch-start] Next steps:"
|
|
1301
500
|
echo " cd \"${worktree_path}\""
|
|
1302
501
|
echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
|
|
1303
502
|
echo " # implement + commit"
|
|
1304
|
-
echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base
|
|
503
|
+
echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base dev --via-pr --wait-for-merge"
|