@imdeadpool/guardex 5.0.8 → 5.0.11
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 +99 -0
- package/bin/multiagent-safety.js +980 -11
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +12 -3
- package/templates/scripts/agent-branch-finish.sh +2 -2
- package/templates/scripts/agent-branch-start.sh +109 -21
- package/templates/scripts/agent-worktree-prune.sh +102 -2
- package/templates/scripts/codex-agent.sh +91 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.11",
|
|
4
4
|
"description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"agent:safety:scan": "gx scan",
|
|
30
30
|
"agent:safety:fix": "gx fix",
|
|
31
31
|
"agent:safety:doctor": "gx doctor",
|
|
32
|
-
"agent:review:watch": "bash ./scripts/review-bot-watch.sh"
|
|
32
|
+
"agent:review:watch": "bash ./scripts/review-bot-watch.sh",
|
|
33
|
+
"agent:finish": "gx finish --all"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=18"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- multiagent-safety:START -->
|
|
2
|
-
## Multi-Agent Execution Contract (
|
|
2
|
+
## Multi-Agent Execution Contract (GX)
|
|
3
3
|
|
|
4
4
|
0. Session plan comment + read gate (required)
|
|
5
5
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
|
|
14
14
|
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
|
|
15
15
|
- Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
|
|
16
|
+
- OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR for those changes (via `codex-agent` or `agent-branch-finish`).
|
|
16
17
|
- Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
|
|
17
18
|
- Use `--no-cleanup` only when you explicitly need to keep a merged sandbox for audit/debug follow-up.
|
|
18
19
|
- If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge` and keep the branch open until checks/review pass.
|
|
@@ -45,9 +46,17 @@
|
|
|
45
46
|
- Verification commands + results
|
|
46
47
|
- Risks / follow-ups
|
|
47
48
|
|
|
48
|
-
## OpenSpec Plan Workspace (
|
|
49
|
+
## OpenSpec Plan Workspace (required for agent sub-branch changes)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
OMX Codex execution flows must use OpenSpec. `scripts/codex-agent.sh` bootstraps a
|
|
52
|
+
per-branch plan workspace automatically under:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
openspec/plan/<agent-branch-slug>/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For manual `scripts/agent-branch-start.sh` usage, enable auto-bootstrap with
|
|
59
|
+
`MUSAFETY_OPENSPEC_AUTO_INIT=true` or scaffold manually before implementation:
|
|
51
60
|
|
|
52
61
|
```bash
|
|
53
62
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -338,7 +338,7 @@ is_local_branch_delete_error() {
|
|
|
338
338
|
|
|
339
339
|
read_pr_state() {
|
|
340
340
|
local state_line
|
|
341
|
-
state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] |
|
|
341
|
+
state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
|
|
342
342
|
if [[ -z "$state_line" ]]; then
|
|
343
343
|
return 1
|
|
344
344
|
fi
|
|
@@ -346,7 +346,7 @@ read_pr_state() {
|
|
|
346
346
|
local parsed_state=""
|
|
347
347
|
local parsed_merged_at=""
|
|
348
348
|
local parsed_url=""
|
|
349
|
-
IFS=$'\
|
|
349
|
+
IFS=$'\x1f' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
|
|
350
350
|
PR_STATE="$parsed_state"
|
|
351
351
|
PR_MERGED_AT="$parsed_merged_at"
|
|
352
352
|
if [[ -n "$parsed_url" ]]; then
|
|
@@ -6,6 +6,8 @@ AGENT_NAME="agent"
|
|
|
6
6
|
BASE_BRANCH=""
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
8
|
WORKTREE_ROOT_REL=".omx/agent-worktrees"
|
|
9
|
+
OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-false}"
|
|
10
|
+
OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
|
|
9
11
|
POSITIONAL_ARGS=()
|
|
10
12
|
|
|
11
13
|
while [[ $# -gt 0 ]]; do
|
|
@@ -82,6 +84,31 @@ sanitize_slug() {
|
|
|
82
84
|
printf '%s' "$slug"
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
normalize_bool() {
|
|
88
|
+
local raw="${1:-}"
|
|
89
|
+
local fallback="${2:-0}"
|
|
90
|
+
local lowered
|
|
91
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
92
|
+
case "$lowered" in
|
|
93
|
+
1|true|yes|on) printf '1' ;;
|
|
94
|
+
0|false|no|off) printf '0' ;;
|
|
95
|
+
'') printf '%s' "$fallback" ;;
|
|
96
|
+
*) printf '%s' "$fallback" ;;
|
|
97
|
+
esac
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
101
|
+
|
|
102
|
+
resolve_openspec_plan_slug() {
|
|
103
|
+
local branch_name="$1"
|
|
104
|
+
local task_slug="$2"
|
|
105
|
+
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
106
|
+
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
107
|
+
return 0
|
|
108
|
+
fi
|
|
109
|
+
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
resolve_active_codex_snapshot_name() {
|
|
86
113
|
local override="${MUSAFETY_CODEX_AUTH_SNAPSHOT:-}"
|
|
87
114
|
if [[ -n "$override" ]]; then
|
|
@@ -114,17 +141,6 @@ has_local_changes() {
|
|
|
114
141
|
return 1
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
has_tracked_changes() {
|
|
118
|
-
local root="$1"
|
|
119
|
-
if ! git -C "$root" diff --quiet; then
|
|
120
|
-
return 0
|
|
121
|
-
fi
|
|
122
|
-
if ! git -C "$root" diff --cached --quiet; then
|
|
123
|
-
return 0
|
|
124
|
-
fi
|
|
125
|
-
return 1
|
|
126
|
-
}
|
|
127
|
-
|
|
128
144
|
resolve_protected_branches() {
|
|
129
145
|
local root="$1"
|
|
130
146
|
local raw
|
|
@@ -177,6 +193,63 @@ hydrate_local_helper_in_worktree() {
|
|
|
177
193
|
echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}"
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
hydrate_dependency_dir_symlink_in_worktree() {
|
|
197
|
+
local repo="$1"
|
|
198
|
+
local worktree="$2"
|
|
199
|
+
local relative_path="$3"
|
|
200
|
+
local source_path="${repo}/${relative_path}"
|
|
201
|
+
local target_path="${worktree}/${relative_path}"
|
|
202
|
+
|
|
203
|
+
if [[ ! -d "$source_path" ]]; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [[ -e "$target_path" ]]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
mkdir -p "$(dirname "$target_path")"
|
|
212
|
+
ln -s "$source_path" "$target_path"
|
|
213
|
+
echo "[agent-branch-start] Linked dependency dir in worktree: ${relative_path}"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
initialize_openspec_plan_workspace() {
|
|
217
|
+
local repo="$1"
|
|
218
|
+
local worktree="$2"
|
|
219
|
+
local plan_slug="$3"
|
|
220
|
+
|
|
221
|
+
hydrate_local_helper_in_worktree "$repo" "$worktree" "scripts/openspec/init-plan-workspace.sh"
|
|
222
|
+
|
|
223
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
|
|
224
|
+
return 0
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
local openspec_script="${worktree}/scripts/openspec/init-plan-workspace.sh"
|
|
228
|
+
if [[ ! -f "$openspec_script" ]]; then
|
|
229
|
+
echo "[agent-branch-start] OpenSpec init script is missing in sandbox worktree." >&2
|
|
230
|
+
echo "[agent-branch-start] Run 'gx setup --target \"$repo\"' to repair templates, then retry." >&2
|
|
231
|
+
return 1
|
|
232
|
+
fi
|
|
233
|
+
if [[ ! -x "$openspec_script" ]]; then
|
|
234
|
+
chmod +x "$openspec_script" 2>/dev/null || true
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
local init_output=""
|
|
238
|
+
if ! init_output="$(
|
|
239
|
+
cd "$worktree"
|
|
240
|
+
bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
|
|
241
|
+
)"; then
|
|
242
|
+
printf '%s\n' "$init_output" >&2
|
|
243
|
+
echo "[agent-branch-start] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
|
|
244
|
+
return 1
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
if [[ -n "$init_output" ]]; then
|
|
248
|
+
printf '%s\n' "$init_output"
|
|
249
|
+
fi
|
|
250
|
+
echo "[agent-branch-start] OpenSpec plan workspace: ${worktree}/openspec/plan/${plan_slug}"
|
|
251
|
+
}
|
|
252
|
+
|
|
180
253
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
181
254
|
echo "[agent-branch-start] Not inside a git repository." >&2
|
|
182
255
|
exit 1
|
|
@@ -190,12 +263,15 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
|
190
263
|
fi
|
|
191
264
|
|
|
192
265
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
266
|
+
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
267
|
+
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
268
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
269
|
+
BASE_BRANCH="$current_branch"
|
|
196
270
|
else
|
|
197
|
-
|
|
198
|
-
if [[ -n "$
|
|
271
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
272
|
+
if [[ -n "$configured_base" ]]; then
|
|
273
|
+
BASE_BRANCH="$configured_base"
|
|
274
|
+
elif [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
|
|
199
275
|
BASE_BRANCH="$current_branch"
|
|
200
276
|
else
|
|
201
277
|
BASE_BRANCH="dev"
|
|
@@ -235,6 +311,7 @@ done
|
|
|
235
311
|
worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
|
|
236
312
|
mkdir -p "$worktree_root"
|
|
237
313
|
worktree_path="${worktree_root}/${branch_name//\//__}"
|
|
314
|
+
openspec_plan_slug="$(resolve_openspec_plan_slug "$branch_name" "$task_slug")"
|
|
238
315
|
|
|
239
316
|
if [[ -e "$worktree_path" ]]; then
|
|
240
317
|
echo "[agent-branch-start] Worktree path already exists: ${worktree_path}" >&2
|
|
@@ -243,10 +320,11 @@ fi
|
|
|
243
320
|
|
|
244
321
|
auto_transfer_stash_ref=""
|
|
245
322
|
auto_transfer_message=""
|
|
323
|
+
auto_transfer_source_branch=""
|
|
246
324
|
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
247
325
|
protected_branches_raw="$(resolve_protected_branches "$repo_root")"
|
|
248
|
-
if [[ "$current_branch"
|
|
249
|
-
if
|
|
326
|
+
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
|
|
327
|
+
if has_local_changes "$repo_root"; then
|
|
250
328
|
auto_transfer_message="musafety-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
|
|
251
329
|
if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
|
|
252
330
|
auto_transfer_stash_ref="$(
|
|
@@ -254,7 +332,8 @@ if [[ "$current_branch" == "$BASE_BRANCH" ]] && is_protected_branch_name "$BASE_
|
|
|
254
332
|
| awk -v msg="$auto_transfer_message" '$0 ~ msg { ref=$1; sub(/:$/, "", ref); print ref; exit }'
|
|
255
333
|
)"
|
|
256
334
|
if [[ -n "$auto_transfer_stash_ref" ]]; then
|
|
257
|
-
|
|
335
|
+
auto_transfer_source_branch="$current_branch"
|
|
336
|
+
echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
|
|
258
337
|
fi
|
|
259
338
|
fi
|
|
260
339
|
fi
|
|
@@ -270,19 +349,28 @@ fi
|
|
|
270
349
|
if [[ -n "$auto_transfer_stash_ref" ]]; then
|
|
271
350
|
if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
|
|
272
351
|
git -C "$repo_root" stash drop "$auto_transfer_stash_ref" >/dev/null 2>&1 || true
|
|
273
|
-
|
|
352
|
+
transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
|
|
353
|
+
echo "[agent-branch-start] Moved local changes from '${transfer_label}' into '${branch_name}'."
|
|
274
354
|
else
|
|
275
355
|
echo "[agent-branch-start] Failed to auto-apply moved changes in new worktree." >&2
|
|
276
|
-
|
|
356
|
+
transfer_label="${auto_transfer_source_branch:-$BASE_BRANCH}"
|
|
357
|
+
echo "[agent-branch-start] Changes are preserved in ${auto_transfer_stash_ref} on ${transfer_label}." >&2
|
|
277
358
|
echo "[agent-branch-start] Apply manually with: git -C \"$worktree_path\" stash apply \"${auto_transfer_stash_ref}\"" >&2
|
|
278
359
|
exit 1
|
|
279
360
|
fi
|
|
280
361
|
fi
|
|
281
362
|
|
|
282
363
|
hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-agent.sh"
|
|
364
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
|
|
365
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
|
|
366
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
|
|
367
|
+
if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
|
|
368
|
+
exit 1
|
|
369
|
+
fi
|
|
283
370
|
|
|
284
371
|
echo "[agent-branch-start] Created branch: ${branch_name}"
|
|
285
372
|
echo "[agent-branch-start] Worktree: ${worktree_path}"
|
|
373
|
+
echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
|
|
286
374
|
echo "[agent-branch-start] Next steps:"
|
|
287
375
|
echo " cd \"${worktree_path}\""
|
|
288
376
|
echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
|
|
@@ -9,6 +9,10 @@ DELETE_BRANCHES=0
|
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
10
|
ONLY_DIRTY_WORKTREES=0
|
|
11
11
|
TARGET_BRANCH=""
|
|
12
|
+
IDLE_MINUTES=0
|
|
13
|
+
NOW_EPOCH_RAW="${MUSAFETY_PRUNE_NOW_EPOCH:-}"
|
|
14
|
+
IDLE_SECONDS=0
|
|
15
|
+
NOW_EPOCH=0
|
|
12
16
|
|
|
13
17
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
14
18
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -45,9 +49,13 @@ while [[ $# -gt 0 ]]; do
|
|
|
45
49
|
TARGET_BRANCH="${2:-}"
|
|
46
50
|
shift 2
|
|
47
51
|
;;
|
|
52
|
+
--idle-minutes)
|
|
53
|
+
IDLE_MINUTES="${2:-}"
|
|
54
|
+
shift 2
|
|
55
|
+
;;
|
|
48
56
|
*)
|
|
49
57
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
50
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
|
|
58
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
|
|
51
59
|
exit 1
|
|
52
60
|
;;
|
|
53
61
|
esac
|
|
@@ -103,6 +111,16 @@ if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
|
|
|
103
111
|
exit 1
|
|
104
112
|
fi
|
|
105
113
|
|
|
114
|
+
if [[ ! "$IDLE_MINUTES" =~ ^[0-9]+$ ]]; then
|
|
115
|
+
echo "[agent-worktree-prune] --idle-minutes must be an integer >= 0." >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if [[ -n "$NOW_EPOCH_RAW" && ! "$NOW_EPOCH_RAW" =~ ^[0-9]+$ ]]; then
|
|
120
|
+
echo "[agent-worktree-prune] MUSAFETY_PRUNE_NOW_EPOCH must be a unix timestamp integer." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
106
124
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
107
125
|
BASE_BRANCH="$(resolve_base_branch)"
|
|
108
126
|
fi
|
|
@@ -117,6 +135,13 @@ if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}";
|
|
|
117
135
|
exit 1
|
|
118
136
|
fi
|
|
119
137
|
|
|
138
|
+
IDLE_SECONDS=$((IDLE_MINUTES * 60))
|
|
139
|
+
if [[ -n "$NOW_EPOCH_RAW" ]]; then
|
|
140
|
+
NOW_EPOCH="$NOW_EPOCH_RAW"
|
|
141
|
+
else
|
|
142
|
+
NOW_EPOCH="$(date +%s)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
120
145
|
run_cmd() {
|
|
121
146
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
122
147
|
echo "[agent-worktree-prune] [dry-run] $*"
|
|
@@ -167,6 +192,71 @@ select_unique_worktree_path() {
|
|
|
167
192
|
printf '%s' "$candidate"
|
|
168
193
|
}
|
|
169
194
|
|
|
195
|
+
read_branch_activity_epoch() {
|
|
196
|
+
local branch="$1"
|
|
197
|
+
local wt="${2:-}"
|
|
198
|
+
local activity_epoch=""
|
|
199
|
+
|
|
200
|
+
activity_epoch="$(
|
|
201
|
+
git -C "$repo_root" reflog show --format='%ct' -n 1 "refs/heads/${branch}" 2>/dev/null \
|
|
202
|
+
| head -n 1 \
|
|
203
|
+
| tr -d '[:space:]'
|
|
204
|
+
)"
|
|
205
|
+
if [[ -z "$activity_epoch" ]]; then
|
|
206
|
+
activity_epoch="$(
|
|
207
|
+
git -C "$repo_root" log -1 --format='%ct' "$branch" 2>/dev/null \
|
|
208
|
+
| head -n 1 \
|
|
209
|
+
| tr -d '[:space:]'
|
|
210
|
+
)"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [[ -n "$wt" && -d "$wt" ]]; then
|
|
214
|
+
local lock_file="${wt}/.omx/state/agent-file-locks.json"
|
|
215
|
+
if [[ -f "$lock_file" ]]; then
|
|
216
|
+
local lock_mtime=""
|
|
217
|
+
lock_mtime="$(stat -c %Y "$lock_file" 2>/dev/null || stat -f %m "$lock_file" 2>/dev/null || true)"
|
|
218
|
+
if [[ "$lock_mtime" =~ ^[0-9]+$ ]]; then
|
|
219
|
+
if [[ -z "$activity_epoch" || "$lock_mtime" -gt "$activity_epoch" ]]; then
|
|
220
|
+
activity_epoch="$lock_mtime"
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
printf '%s' "$activity_epoch"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
skipped_recent=0
|
|
230
|
+
|
|
231
|
+
branch_idle_gate() {
|
|
232
|
+
local branch="$1"
|
|
233
|
+
local wt="$2"
|
|
234
|
+
local reason="$3"
|
|
235
|
+
if [[ "$IDLE_SECONDS" -le 0 ]]; then
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
if [[ -z "$branch" ]]; then
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
local last_activity_epoch=""
|
|
243
|
+
last_activity_epoch="$(read_branch_activity_epoch "$branch" "$wt")"
|
|
244
|
+
if [[ ! "$last_activity_epoch" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local idle_age=$((NOW_EPOCH - last_activity_epoch))
|
|
249
|
+
if [[ "$idle_age" -lt 0 ]]; then
|
|
250
|
+
idle_age=0
|
|
251
|
+
fi
|
|
252
|
+
if [[ "$idle_age" -lt "$IDLE_SECONDS" ]]; then
|
|
253
|
+
skipped_recent=$((skipped_recent + 1))
|
|
254
|
+
echo "[agent-worktree-prune] Skipping recent branch (${reason}): ${branch} (idle=${idle_age}s < ${IDLE_SECONDS}s)"
|
|
255
|
+
return 1
|
|
256
|
+
fi
|
|
257
|
+
return 0
|
|
258
|
+
}
|
|
259
|
+
|
|
170
260
|
relocated_foreign=0
|
|
171
261
|
skipped_foreign=0
|
|
172
262
|
|
|
@@ -273,6 +363,10 @@ process_entry() {
|
|
|
273
363
|
return
|
|
274
364
|
fi
|
|
275
365
|
|
|
366
|
+
if ! branch_idle_gate "$branch" "$wt" "$remove_reason"; then
|
|
367
|
+
return
|
|
368
|
+
fi
|
|
369
|
+
|
|
276
370
|
if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
|
|
277
371
|
skipped_dirty=$((skipped_dirty + 1))
|
|
278
372
|
echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
|
|
@@ -339,6 +433,9 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
|
339
433
|
if branch_has_worktree "$branch"; then
|
|
340
434
|
continue
|
|
341
435
|
fi
|
|
436
|
+
if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
|
|
437
|
+
continue
|
|
438
|
+
fi
|
|
342
439
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
343
440
|
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
344
441
|
removed_branches=$((removed_branches + 1))
|
|
@@ -356,7 +453,7 @@ fi
|
|
|
356
453
|
|
|
357
454
|
run_cmd git -C "$repo_root" worktree prune
|
|
358
455
|
|
|
359
|
-
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
|
|
456
|
+
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, idle_minutes=${IDLE_MINUTES}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}, skipped_recent=${skipped_recent}"
|
|
360
457
|
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
|
|
361
458
|
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
|
|
362
459
|
fi
|
|
@@ -366,3 +463,6 @@ fi
|
|
|
366
463
|
if [[ "$skipped_dirty" -gt 0 ]]; then
|
|
367
464
|
echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
|
|
368
465
|
fi
|
|
466
|
+
if [[ "$IDLE_SECONDS" -gt 0 && "$skipped_recent" -gt 0 ]]; then
|
|
467
|
+
echo "[agent-worktree-prune] Tip: recent branches were preserved by --idle-minutes=${IDLE_MINUTES}. Re-run later or lower the threshold." >&2
|
|
468
|
+
fi
|
|
@@ -10,6 +10,8 @@ AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
|
|
|
10
10
|
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
|
|
11
11
|
AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-true}"
|
|
12
12
|
AUTO_WAIT_FOR_MERGE_RAW="${MUSAFETY_CODEX_WAIT_FOR_MERGE:-true}"
|
|
13
|
+
OPENSPEC_AUTO_INIT_RAW="${MUSAFETY_OPENSPEC_AUTO_INIT:-true}"
|
|
14
|
+
OPENSPEC_PLAN_SLUG_OVERRIDE="${MUSAFETY_OPENSPEC_PLAN_SLUG:-}"
|
|
13
15
|
|
|
14
16
|
normalize_bool() {
|
|
15
17
|
local raw="${1:-}"
|
|
@@ -28,6 +30,7 @@ AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
|
|
|
28
30
|
AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"
|
|
29
31
|
AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "1")"
|
|
30
32
|
AUTO_WAIT_FOR_MERGE="$(normalize_bool "$AUTO_WAIT_FOR_MERGE_RAW" "1")"
|
|
33
|
+
OPENSPEC_AUTO_INIT="$(normalize_bool "$OPENSPEC_AUTO_INIT_RAW" "1")"
|
|
31
34
|
|
|
32
35
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
33
36
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -136,6 +139,46 @@ sanitize_slug() {
|
|
|
136
139
|
printf '%s' "$slug"
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
resolve_openspec_plan_slug() {
|
|
143
|
+
local branch_name="$1"
|
|
144
|
+
local task_slug
|
|
145
|
+
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
146
|
+
if [[ -n "$OPENSPEC_PLAN_SLUG_OVERRIDE" ]]; then
|
|
147
|
+
sanitize_slug "$OPENSPEC_PLAN_SLUG_OVERRIDE" "$task_slug"
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
sanitize_slug "${branch_name//\//-}" "$task_slug"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
hydrate_local_helper_in_worktree() {
|
|
154
|
+
local worktree="$1"
|
|
155
|
+
local relative_path="$2"
|
|
156
|
+
local worktree_target="${worktree}/${relative_path}"
|
|
157
|
+
local source_path=""
|
|
158
|
+
|
|
159
|
+
if [[ -e "$worktree_target" ]]; then
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if [[ -f "${repo_root}/${relative_path}" ]]; then
|
|
164
|
+
source_path="${repo_root}/${relative_path}"
|
|
165
|
+
elif [[ -f "${repo_root}/templates/${relative_path}" ]]; then
|
|
166
|
+
source_path="${repo_root}/templates/${relative_path}"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [[ -z "$source_path" ]]; then
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
mkdir -p "$(dirname "$worktree_target")"
|
|
174
|
+
cp "$source_path" "$worktree_target"
|
|
175
|
+
if [[ -x "$source_path" ]]; then
|
|
176
|
+
chmod +x "$worktree_target"
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
echo "[codex-agent] Hydrated local helper in sandbox: ${relative_path}"
|
|
180
|
+
}
|
|
181
|
+
|
|
139
182
|
resolve_start_base_branch() {
|
|
140
183
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
141
184
|
printf '%s' "$BASE_BRANCH"
|
|
@@ -239,7 +282,7 @@ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/nu
|
|
|
239
282
|
start_output=""
|
|
240
283
|
start_status=0
|
|
241
284
|
set +e
|
|
242
|
-
start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
|
|
285
|
+
start_output="$(MUSAFETY_OPENSPEC_AUTO_INIT=0 bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
|
|
243
286
|
start_status=$?
|
|
244
287
|
set -e
|
|
245
288
|
|
|
@@ -363,6 +406,43 @@ sync_worktree_with_base() {
|
|
|
363
406
|
return 0
|
|
364
407
|
}
|
|
365
408
|
|
|
409
|
+
ensure_openspec_plan_workspace() {
|
|
410
|
+
local wt="$1"
|
|
411
|
+
local branch="$2"
|
|
412
|
+
|
|
413
|
+
if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
|
|
414
|
+
return 0
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
hydrate_local_helper_in_worktree "$wt" "scripts/openspec/init-plan-workspace.sh"
|
|
418
|
+
|
|
419
|
+
local openspec_script="${wt}/scripts/openspec/init-plan-workspace.sh"
|
|
420
|
+
if [[ ! -f "$openspec_script" ]]; then
|
|
421
|
+
echo "[codex-agent] Missing OpenSpec init script in sandbox: ${openspec_script}" >&2
|
|
422
|
+
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and retry." >&2
|
|
423
|
+
return 1
|
|
424
|
+
fi
|
|
425
|
+
if [[ ! -x "$openspec_script" ]]; then
|
|
426
|
+
chmod +x "$openspec_script" 2>/dev/null || true
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
local plan_slug
|
|
430
|
+
plan_slug="$(resolve_openspec_plan_slug "$branch")"
|
|
431
|
+
local init_output=""
|
|
432
|
+
if ! init_output="$(
|
|
433
|
+
cd "$wt"
|
|
434
|
+
bash "scripts/openspec/init-plan-workspace.sh" "$plan_slug" 2>&1
|
|
435
|
+
)"; then
|
|
436
|
+
printf '%s\n' "$init_output" >&2
|
|
437
|
+
echo "[codex-agent] OpenSpec workspace initialization failed for plan '${plan_slug}'." >&2
|
|
438
|
+
return 1
|
|
439
|
+
fi
|
|
440
|
+
if [[ -n "$init_output" ]]; then
|
|
441
|
+
printf '%s\n' "$init_output"
|
|
442
|
+
fi
|
|
443
|
+
echo "[codex-agent] OpenSpec plan workspace: ${wt}/openspec/plan/${plan_slug}"
|
|
444
|
+
}
|
|
445
|
+
|
|
366
446
|
worktree_has_changes() {
|
|
367
447
|
local wt="$1"
|
|
368
448
|
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
@@ -579,6 +659,16 @@ if ! sync_worktree_with_base "$worktree_path"; then
|
|
|
579
659
|
exit 1
|
|
580
660
|
fi
|
|
581
661
|
|
|
662
|
+
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
663
|
+
if [[ -z "$worktree_branch" || "$worktree_branch" == "HEAD" ]]; then
|
|
664
|
+
echo "[codex-agent] Could not determine sandbox branch for worktree: $worktree_path" >&2
|
|
665
|
+
exit 1
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
if ! ensure_openspec_plan_workspace "$worktree_path" "$worktree_branch"; then
|
|
669
|
+
exit 1
|
|
670
|
+
fi
|
|
671
|
+
|
|
582
672
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
583
673
|
cd "$worktree_path"
|
|
584
674
|
set +e
|
|
@@ -590,8 +680,6 @@ cd "$repo_root"
|
|
|
590
680
|
final_exit="$codex_exit"
|
|
591
681
|
auto_finish_completed=0
|
|
592
682
|
|
|
593
|
-
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
594
|
-
|
|
595
683
|
if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
596
684
|
if [[ "$AUTO_WAIT_FOR_MERGE" -eq 1 && "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
597
685
|
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> wait for merge -> cleanup."
|