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