@imdeadpool/guardex 7.0.13 → 7.0.15
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 +35 -4
- package/bin/multiagent-safety.js +1140 -156
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +16 -9
- package/templates/github/workflows/cr.yml +9 -3
- package/templates/scripts/agent-branch-finish.sh +35 -6
- package/templates/scripts/agent-branch-start.sh +50 -12
- package/templates/scripts/agent-worktree-prune.sh +156 -52
- package/templates/scripts/codex-agent.sh +49 -2
- package/templates/scripts/guardex-docker-loader.sh +123 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.15",
|
|
4
4
|
"description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bugs": {
|
|
61
61
|
"url": "https://github.com/recodeee/gitguardex/issues"
|
|
62
62
|
},
|
|
63
|
-
"homepage": "https://
|
|
63
|
+
"homepage": "https://github.com/recodeee/gitguardex-frontend",
|
|
64
64
|
"funding": "https://github.com/sponsors/recodeecom",
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
<!-- multiagent-safety:START -->
|
|
2
|
-
## Multi-Agent
|
|
2
|
+
## Multi-Agent Execution Contract (GX)
|
|
3
3
|
|
|
4
|
-
**Repo toggle.** `GUARDEX_ON=0
|
|
4
|
+
**Repo toggle.** Guardex is enabled by default. If the repo root `.env` sets `GUARDEX_ON=0`, `false`, `no`, or `off`, treat this entire Guardex contract as disabled for the repo and do not require Guardex worktrees, lock claims, completion flow, or OpenSpec workflow until `GUARDEX_ON` is re-enabled.
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**Repo toggle examples.** Add one of these lines to the repo-root `.env` file:
|
|
7
|
+
`GUARDEX_ON=0` disables Guardex for that repo.
|
|
8
|
+
`GUARDEX_ON=1` explicitly enables Guardex for that repo again.
|
|
7
9
|
|
|
8
|
-
**
|
|
10
|
+
**Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `scripts/agent-branch-start.sh "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions - bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
|
|
11
|
+
For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch instead of creating a fresh lane unless the user explicitly redirects scope.
|
|
12
|
+
Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
9
13
|
|
|
10
|
-
**
|
|
14
|
+
**Ownership.** Before editing, claim files: `scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
|
|
11
15
|
|
|
12
|
-
**
|
|
16
|
+
**Handoff gate.** Post a one-line handoff note (plan/change, owned scope, intended action) before editing. Re-read the latest handoffs before replacing others' code.
|
|
13
17
|
|
|
14
|
-
**
|
|
18
|
+
**Completion.** Finish with `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge --cleanup` (or `gx finish --all`). Task is only complete when: commit pushed, PR URL recorded, state = `MERGED`, sandbox worktree pruned. If anything blocks, append a `BLOCKED:` note and stop - don't half-finish.
|
|
19
|
+
OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR before considering the branch complete.
|
|
15
20
|
|
|
16
|
-
**
|
|
21
|
+
**Parallel safety.** Assume other agents edit nearby. Never revert unrelated changes. Report conflicts in the handoff.
|
|
17
22
|
|
|
18
|
-
**
|
|
23
|
+
**Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
|
|
24
|
+
|
|
25
|
+
**OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `scripts/agent-branch-finish.sh ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
|
|
19
26
|
|
|
20
27
|
**Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
|
|
21
28
|
<!-- multiagent-safety:END -->
|
|
@@ -10,12 +10,18 @@ permissions:
|
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
review:
|
|
13
|
-
if: ${{ secrets.OPENAI_API_KEY != '' }}
|
|
14
13
|
runs-on: ubuntu-latest
|
|
14
|
+
env:
|
|
15
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
15
16
|
steps:
|
|
16
|
-
-
|
|
17
|
+
- name: Skip when OPENAI_API_KEY is missing
|
|
18
|
+
if: ${{ env.OPENAI_API_KEY == '' }}
|
|
19
|
+
run: echo "OPENAI_API_KEY is not configured; skipping Code Review workflow."
|
|
20
|
+
|
|
21
|
+
- uses: anc95/ChatGPT-CodeReview@1e3df152c1b85c12da580b206c91ad343460c584 # v1.0.23
|
|
22
|
+
if: ${{ env.OPENAI_API_KEY != '' }}
|
|
17
23
|
env:
|
|
18
24
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
|
-
OPENAI_API_KEY: ${{
|
|
25
|
+
OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }}
|
|
20
26
|
OPENAI_API_ENDPOINT: https://api.openai.com/v1
|
|
21
27
|
MODEL: gpt-4o-mini
|
|
@@ -144,24 +144,44 @@ else
|
|
|
144
144
|
common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
|
|
145
145
|
fi
|
|
146
146
|
repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
|
|
147
|
-
agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
|
|
148
147
|
|
|
149
148
|
if [[ -z "$SOURCE_BRANCH" ]]; then
|
|
150
149
|
SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
151
150
|
fi
|
|
152
151
|
|
|
152
|
+
stored_worktree_root_rel="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexWorktreeRoot" || true)"
|
|
153
|
+
if [[ -z "$stored_worktree_root_rel" ]]; then
|
|
154
|
+
stored_worktree_root_rel=".omx/agent-worktrees"
|
|
155
|
+
fi
|
|
156
|
+
agent_worktree_root="${repo_common_root}/${stored_worktree_root_rel}"
|
|
157
|
+
|
|
153
158
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
154
159
|
echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
|
|
155
160
|
exit 1
|
|
156
161
|
fi
|
|
157
162
|
|
|
158
163
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
159
|
-
|
|
160
|
-
if [[ -n "$
|
|
161
|
-
BASE_BRANCH="$
|
|
164
|
+
source_branch_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexBase" || true)"
|
|
165
|
+
if [[ -n "$source_branch_base" ]]; then
|
|
166
|
+
BASE_BRANCH="$source_branch_base"
|
|
167
|
+
else
|
|
168
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
169
|
+
if [[ -n "$configured_base" ]]; then
|
|
170
|
+
BASE_BRANCH="$configured_base"
|
|
171
|
+
fi
|
|
162
172
|
fi
|
|
163
173
|
fi
|
|
164
174
|
|
|
175
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
176
|
+
for fallback_branch in dev main master; do
|
|
177
|
+
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback_branch}" \
|
|
178
|
+
|| git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${fallback_branch}"; then
|
|
179
|
+
BASE_BRANCH="$fallback_branch"
|
|
180
|
+
break
|
|
181
|
+
fi
|
|
182
|
+
done
|
|
183
|
+
fi
|
|
184
|
+
|
|
165
185
|
if [[ -z "$BASE_BRANCH" ]]; then
|
|
166
186
|
BASE_BRANCH="dev"
|
|
167
187
|
fi
|
|
@@ -268,8 +288,17 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
|
|
|
268
288
|
fi
|
|
269
289
|
fi
|
|
270
290
|
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
integration_stamp="$(date +%Y%m%d-%H%M%S)"
|
|
292
|
+
integration_worktree_base="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
|
|
293
|
+
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
|
|
294
|
+
integration_worktree="$integration_worktree_base"
|
|
295
|
+
integration_branch="$integration_branch_base"
|
|
296
|
+
integration_suffix=1
|
|
297
|
+
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
298
|
+
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
299
|
+
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
300
|
+
integration_suffix=$((integration_suffix + 1))
|
|
301
|
+
done
|
|
273
302
|
mkdir -p "$(dirname "$integration_worktree")"
|
|
274
303
|
|
|
275
304
|
git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
|
|
@@ -5,7 +5,8 @@ TASK_NAME="task"
|
|
|
5
5
|
AGENT_NAME="agent"
|
|
6
6
|
BASE_BRANCH=""
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
|
-
WORKTREE_ROOT_REL="
|
|
8
|
+
WORKTREE_ROOT_REL=""
|
|
9
|
+
WORKTREE_ROOT_EXPLICIT=0
|
|
9
10
|
OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
|
|
10
11
|
OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
|
|
11
12
|
OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
|
|
@@ -44,6 +45,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
44
45
|
;;
|
|
45
46
|
--worktree-root)
|
|
46
47
|
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
|
|
48
|
+
WORKTREE_ROOT_EXPLICIT=1
|
|
47
49
|
shift 2
|
|
48
50
|
;;
|
|
49
51
|
--)
|
|
@@ -123,12 +125,41 @@ shorten_slug() {
|
|
|
123
125
|
printf '%s' "$shortened"
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
env_flag_truthy() {
|
|
129
|
+
local raw="${1:-}"
|
|
130
|
+
local lowered
|
|
131
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
132
|
+
case "$lowered" in
|
|
133
|
+
1|true|yes|on) return 0 ;;
|
|
134
|
+
*) return 1 ;;
|
|
135
|
+
esac
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
default_worktree_root_rel() {
|
|
139
|
+
local raw_agent="$1"
|
|
140
|
+
local override="${GUARDEX_AGENT_TYPE:-}"
|
|
141
|
+
local lowered_agent lowered_override
|
|
142
|
+
lowered_agent="$(printf '%s' "$raw_agent" | tr '[:upper:]' '[:lower:]')"
|
|
143
|
+
lowered_override="$(printf '%s' "$override" | tr '[:upper:]' '[:lower:]')"
|
|
144
|
+
|
|
145
|
+
if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" ]] || env_flag_truthy "${CLAUDECODE:-}"; then
|
|
146
|
+
printf '.omc/agent-worktrees'
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if [[ "$lowered_agent" == *claude* ]] || [[ "$lowered_override" == *claude* ]]; then
|
|
151
|
+
printf '.omc/agent-worktrees'
|
|
152
|
+
return 0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
printf '.omx/agent-worktrees'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Collapse arbitrary agent identifiers to a clean role token. Priority:
|
|
159
|
+
# GUARDEX_AGENT_TYPE env override, then recognizable claude/codex aliases, then
|
|
160
|
+
# a small legacy compatibility set, then the literal requested role after slug
|
|
161
|
+
# sanitization. This preserves explicit roles such as planner/executor while
|
|
162
|
+
# keeping the older bot -> codex fallback stable for existing callers.
|
|
132
163
|
normalize_role() {
|
|
133
164
|
local raw_agent="$1"
|
|
134
165
|
local override="${GUARDEX_AGENT_TYPE:-}"
|
|
@@ -150,10 +181,13 @@ normalize_role() {
|
|
|
150
181
|
printf 'claude'
|
|
151
182
|
return 0
|
|
152
183
|
fi
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
local sanitized
|
|
185
|
+
sanitized="$(sanitize_slug "$raw_agent" "codex")"
|
|
186
|
+
if [[ "$sanitized" == "bot" ]]; then
|
|
187
|
+
printf 'codex'
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
printf '%s' "$sanitized"
|
|
157
191
|
}
|
|
158
192
|
|
|
159
193
|
# Timestamp the branch/worktree/openspec slug so parallel agents never collide
|
|
@@ -413,6 +447,9 @@ fi
|
|
|
413
447
|
|
|
414
448
|
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
|
|
415
449
|
agent_slug="$(normalize_role "$AGENT_NAME")"
|
|
450
|
+
if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
|
|
451
|
+
WORKTREE_ROOT_REL="$(default_worktree_root_rel "$AGENT_NAME")"
|
|
452
|
+
fi
|
|
416
453
|
branch_timestamp="$(compose_branch_timestamp)"
|
|
417
454
|
branch_descriptor="$(compose_branch_descriptor "$task_slug" "$branch_timestamp")"
|
|
418
455
|
branch_name_base="agent/${agent_slug}/${branch_descriptor}"
|
|
@@ -497,6 +534,7 @@ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "
|
|
|
497
534
|
exit 1
|
|
498
535
|
fi
|
|
499
536
|
git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
537
|
+
git -C "$repo_root" config "branch.${branch_name}.guardexWorktreeRoot" "$WORKTREE_ROOT_REL" >/dev/null 2>&1 || true
|
|
500
538
|
# Fresh agent branches should start unpublished; clear any inherited base-branch tracking.
|
|
501
539
|
git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true
|
|
502
540
|
|
|
@@ -533,4 +571,4 @@ echo "[agent-branch-start] Next steps:"
|
|
|
533
571
|
echo " cd \"${worktree_path}\""
|
|
534
572
|
echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
|
|
535
573
|
echo " # implement + commit"
|
|
536
|
-
echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base
|
|
574
|
+
echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge"
|
|
@@ -8,11 +8,20 @@ FORCE_DIRTY=0
|
|
|
8
8
|
DELETE_BRANCHES=0
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
10
|
ONLY_DIRTY_WORKTREES=0
|
|
11
|
+
INCLUDE_PR_MERGED=0
|
|
11
12
|
TARGET_BRANCH=""
|
|
12
13
|
IDLE_MINUTES=0
|
|
13
14
|
NOW_EPOCH_RAW="${GUARDEX_PRUNE_NOW_EPOCH:-}"
|
|
14
15
|
IDLE_SECONDS=0
|
|
15
16
|
NOW_EPOCH=0
|
|
17
|
+
GH_BIN="${GUARDEX_GH_BIN:-gh}"
|
|
18
|
+
PR_MERGED_LOOKUP_DISABLED=0
|
|
19
|
+
PR_MERGED_LOOKUP_LOADED=0
|
|
20
|
+
declare -A MERGED_PR_BRANCHES=()
|
|
21
|
+
WORKTREE_ROOT_RELS=(
|
|
22
|
+
".omx/agent-worktrees"
|
|
23
|
+
".omc/agent-worktrees"
|
|
24
|
+
)
|
|
16
25
|
|
|
17
26
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
18
27
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -45,6 +54,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
45
54
|
ONLY_DIRTY_WORKTREES=1
|
|
46
55
|
shift
|
|
47
56
|
;;
|
|
57
|
+
--include-pr-merged)
|
|
58
|
+
INCLUDE_PR_MERGED=1
|
|
59
|
+
shift
|
|
60
|
+
;;
|
|
48
61
|
--branch)
|
|
49
62
|
TARGET_BRANCH="${2:-}"
|
|
50
63
|
shift 2
|
|
@@ -55,7 +68,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
55
68
|
;;
|
|
56
69
|
*)
|
|
57
70
|
echo "[agent-worktree-prune] Unknown argument: $1" >&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
|
|
71
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--include-pr-merged] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
|
|
59
72
|
exit 1
|
|
60
73
|
;;
|
|
61
74
|
esac
|
|
@@ -68,13 +81,36 @@ fi
|
|
|
68
81
|
|
|
69
82
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
70
83
|
current_pwd="$(pwd -P)"
|
|
71
|
-
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
72
84
|
repo_common_dir="$(
|
|
73
85
|
git -C "$repo_root" rev-parse --git-common-dir \
|
|
74
86
|
| awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
|
|
75
87
|
)"
|
|
76
88
|
repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
|
|
77
89
|
|
|
90
|
+
resolve_worktree_root_rel_for_entry() {
|
|
91
|
+
local entry="$1"
|
|
92
|
+
case "$entry" in
|
|
93
|
+
*/.omc/agent-worktrees/*)
|
|
94
|
+
printf '%s' '.omc/agent-worktrees'
|
|
95
|
+
;;
|
|
96
|
+
*)
|
|
97
|
+
printf '%s' '.omx/agent-worktrees'
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
is_managed_worktree_path() {
|
|
103
|
+
local entry="$1"
|
|
104
|
+
local rel root
|
|
105
|
+
for rel in "${WORKTREE_ROOT_RELS[@]}"; do
|
|
106
|
+
root="${repo_root}/${rel}"
|
|
107
|
+
if [[ "$entry" == "${root}"/* ]]; then
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
resolve_base_branch() {
|
|
79
115
|
local configured=""
|
|
80
116
|
local current=""
|
|
@@ -101,6 +137,44 @@ resolve_base_branch() {
|
|
|
101
137
|
printf '%s' ""
|
|
102
138
|
}
|
|
103
139
|
|
|
140
|
+
load_merged_pr_branches() {
|
|
141
|
+
if [[ "$INCLUDE_PR_MERGED" -ne 1 ]]; then
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
if [[ "$PR_MERGED_LOOKUP_DISABLED" -eq 1 ]]; then
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
if [[ "$PR_MERGED_LOOKUP_LOADED" -eq 1 ]]; then
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
if ! command -v "$GH_BIN" >/dev/null 2>&1; then
|
|
151
|
+
PR_MERGED_LOOKUP_DISABLED=1
|
|
152
|
+
return 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
local merged_branches=""
|
|
156
|
+
merged_branches="$(
|
|
157
|
+
"$GH_BIN" pr list --state merged --base "$BASE_BRANCH" --limit 200 --json headRefName --jq '.[].headRefName' 2>/dev/null || true
|
|
158
|
+
)"
|
|
159
|
+
if [[ -n "$merged_branches" ]]; then
|
|
160
|
+
while IFS= read -r merged_branch; do
|
|
161
|
+
[[ -z "$merged_branch" ]] && continue
|
|
162
|
+
MERGED_PR_BRANCHES["$merged_branch"]=1
|
|
163
|
+
done <<< "$merged_branches"
|
|
164
|
+
fi
|
|
165
|
+
PR_MERGED_LOOKUP_LOADED=1
|
|
166
|
+
return 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
branch_has_merged_pr() {
|
|
170
|
+
local branch="$1"
|
|
171
|
+
if [[ "$INCLUDE_PR_MERGED" -ne 1 ]]; then
|
|
172
|
+
return 1
|
|
173
|
+
fi
|
|
174
|
+
load_merged_pr_branches || return 1
|
|
175
|
+
[[ -n "${MERGED_PR_BRANCHES[$branch]:-}" ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
105
179
|
echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
|
|
106
180
|
exit 1
|
|
@@ -261,54 +335,59 @@ relocated_foreign=0
|
|
|
261
335
|
skipped_foreign=0
|
|
262
336
|
|
|
263
337
|
relocate_foreign_worktree_entries() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
|
|
275
|
-
[[ -n "$entry_common_dir" ]] || continue
|
|
338
|
+
local rel="" worktree_root="" entry=""
|
|
339
|
+
for rel in "${WORKTREE_ROOT_RELS[@]}"; do
|
|
340
|
+
worktree_root="${repo_root}/${rel}"
|
|
341
|
+
[[ -d "$worktree_root" ]] || continue
|
|
342
|
+
|
|
343
|
+
for entry in "${worktree_root}"/*; do
|
|
344
|
+
[[ -d "$entry" ]] || continue
|
|
345
|
+
if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
346
|
+
continue
|
|
347
|
+
fi
|
|
276
348
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
349
|
+
local entry_common_dir=""
|
|
350
|
+
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
|
|
351
|
+
[[ -n "$entry_common_dir" ]] || continue
|
|
280
352
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
continue
|
|
285
|
-
fi
|
|
353
|
+
if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
|
|
354
|
+
continue
|
|
355
|
+
fi
|
|
286
356
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
357
|
+
if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
|
|
358
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
359
|
+
echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
|
|
360
|
+
continue
|
|
361
|
+
fi
|
|
292
362
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
363
|
+
local owner_repo_root
|
|
364
|
+
owner_repo_root="$(dirname "$entry_common_dir")"
|
|
365
|
+
local owner_worktree_root_rel owner_worktree_root
|
|
366
|
+
owner_worktree_root_rel="$(resolve_worktree_root_rel_for_entry "$entry")"
|
|
367
|
+
owner_worktree_root="${owner_repo_root}/${owner_worktree_root_rel}"
|
|
368
|
+
local target_path
|
|
369
|
+
target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
|
|
370
|
+
|
|
371
|
+
if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
|
|
372
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
373
|
+
echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
|
|
374
|
+
continue
|
|
375
|
+
fi
|
|
298
376
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
377
|
+
echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
|
|
378
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
379
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
380
|
+
continue
|
|
381
|
+
fi
|
|
304
382
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
383
|
+
mkdir -p "$owner_worktree_root"
|
|
384
|
+
if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
|
|
385
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
386
|
+
else
|
|
387
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
388
|
+
echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
|
|
389
|
+
fi
|
|
390
|
+
done
|
|
312
391
|
done
|
|
313
392
|
}
|
|
314
393
|
|
|
@@ -324,7 +403,9 @@ process_entry() {
|
|
|
324
403
|
local branch_ref="$2"
|
|
325
404
|
|
|
326
405
|
[[ -z "$wt" ]] && return
|
|
327
|
-
|
|
406
|
+
if ! is_managed_worktree_path "$wt"; then
|
|
407
|
+
return
|
|
408
|
+
fi
|
|
328
409
|
|
|
329
410
|
local branch=""
|
|
330
411
|
if [[ -n "$branch_ref" ]]; then
|
|
@@ -342,6 +423,7 @@ process_entry() {
|
|
|
342
423
|
fi
|
|
343
424
|
|
|
344
425
|
local remove_reason=""
|
|
426
|
+
local branch_delete_mode="safe"
|
|
345
427
|
|
|
346
428
|
if [[ -z "$branch_ref" ]]; then
|
|
347
429
|
remove_reason="detached-worktree"
|
|
@@ -352,6 +434,9 @@ process_entry() {
|
|
|
352
434
|
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
353
435
|
remove_reason="merged-agent-branch"
|
|
354
436
|
fi
|
|
437
|
+
elif [[ "$DELETE_BRANCHES" -eq 1 ]] && branch_has_merged_pr "$branch"; then
|
|
438
|
+
remove_reason="merged-agent-pr"
|
|
439
|
+
branch_delete_mode="force"
|
|
355
440
|
elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
|
|
356
441
|
remove_reason="clean-agent-worktree"
|
|
357
442
|
fi
|
|
@@ -383,13 +468,19 @@ process_entry() {
|
|
|
383
468
|
|
|
384
469
|
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
|
|
385
470
|
if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
386
|
-
|
|
471
|
+
local delete_flag="-d"
|
|
472
|
+
local deleted_label="merged"
|
|
473
|
+
if [[ "$branch_delete_mode" == "force" ]]; then
|
|
474
|
+
delete_flag="-D"
|
|
475
|
+
deleted_label="merged PR"
|
|
476
|
+
fi
|
|
477
|
+
if run_cmd git -C "$repo_root" branch "$delete_flag" "$branch" >/dev/null 2>&1; then
|
|
387
478
|
removed_branches=$((removed_branches + 1))
|
|
388
|
-
echo "[agent-worktree-prune] Deleted
|
|
479
|
+
echo "[agent-worktree-prune] Deleted ${deleted_label} branch: ${branch}"
|
|
389
480
|
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
390
481
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
391
482
|
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
392
|
-
echo "[agent-worktree-prune] Deleted
|
|
483
|
+
echo "[agent-worktree-prune] Deleted ${deleted_label} remote branch: ${branch}"
|
|
393
484
|
fi
|
|
394
485
|
fi
|
|
395
486
|
fi
|
|
@@ -420,7 +511,7 @@ while IFS= read -r line || [[ -n "$line" ]]; do
|
|
|
420
511
|
current_branch_ref="${line#branch }"
|
|
421
512
|
;;
|
|
422
513
|
esac
|
|
423
|
-
done < <(git -C "$repo_root" worktree list --porcelain)
|
|
514
|
+
done < <(git -C "$repo_root" worktree list --porcelain)
|
|
424
515
|
|
|
425
516
|
process_entry "$current_wt" "$current_branch_ref"
|
|
426
517
|
|
|
@@ -436,14 +527,27 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
|
436
527
|
if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
|
|
437
528
|
continue
|
|
438
529
|
fi
|
|
530
|
+
merged_by_ancestor=0
|
|
531
|
+
merged_by_pr=0
|
|
439
532
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
440
|
-
|
|
533
|
+
merged_by_ancestor=1
|
|
534
|
+
elif branch_has_merged_pr "$branch"; then
|
|
535
|
+
merged_by_pr=1
|
|
536
|
+
fi
|
|
537
|
+
if [[ "$merged_by_ancestor" -eq 1 || "$merged_by_pr" -eq 1 ]]; then
|
|
538
|
+
delete_flag="-d"
|
|
539
|
+
deleted_label="merged"
|
|
540
|
+
if [[ "$merged_by_pr" -eq 1 && "$merged_by_ancestor" -eq 0 ]]; then
|
|
541
|
+
delete_flag="-D"
|
|
542
|
+
deleted_label="merged PR"
|
|
543
|
+
fi
|
|
544
|
+
if run_cmd git -C "$repo_root" branch "$delete_flag" "$branch" >/dev/null 2>&1; then
|
|
441
545
|
removed_branches=$((removed_branches + 1))
|
|
442
|
-
echo "[agent-worktree-prune] Deleted stale
|
|
546
|
+
echo "[agent-worktree-prune] Deleted stale ${deleted_label} branch: ${branch}"
|
|
443
547
|
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
444
548
|
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
445
549
|
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
446
|
-
echo "[agent-worktree-prune] Deleted stale
|
|
550
|
+
echo "[agent-worktree-prune] Deleted stale ${deleted_label} remote branch: ${branch}"
|
|
447
551
|
fi
|
|
448
552
|
fi
|
|
449
553
|
fi
|
|
@@ -249,6 +249,35 @@ resolve_start_ref() {
|
|
|
249
249
|
return 1
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
origin_remote_looks_like_github() {
|
|
253
|
+
local wt="$1"
|
|
254
|
+
local origin_url=""
|
|
255
|
+
origin_url="$(git -C "$wt" remote get-url origin 2>/dev/null || true)"
|
|
256
|
+
[[ -n "$origin_url" && "$origin_url" =~ github\.com[:/] ]]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
auto_finish_context_is_ready() {
|
|
260
|
+
local wt="$1"
|
|
261
|
+
local gh_bin="${GUARDEX_GH_BIN:-gh}"
|
|
262
|
+
|
|
263
|
+
if ! git -C "$wt" remote get-url origin >/dev/null 2>&1; then
|
|
264
|
+
return 1
|
|
265
|
+
fi
|
|
266
|
+
if ! command -v "$gh_bin" >/dev/null 2>&1; then
|
|
267
|
+
return 1
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
if [[ -n "${GUARDEX_GH_BIN:-}" ]]; then
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if ! origin_remote_looks_like_github "$wt"; then
|
|
275
|
+
return 1
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
"$gh_bin" auth status >/dev/null 2>&1
|
|
279
|
+
}
|
|
280
|
+
|
|
252
281
|
restore_repo_branch_if_changed() {
|
|
253
282
|
local expected_branch="$1"
|
|
254
283
|
if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
|
|
@@ -372,6 +401,17 @@ has_origin_remote() {
|
|
|
372
401
|
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
|
|
373
402
|
}
|
|
374
403
|
|
|
404
|
+
origin_remote_supports_pr_finish() {
|
|
405
|
+
local origin_url
|
|
406
|
+
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
|
407
|
+
case "$origin_url" in
|
|
408
|
+
''|/*|./*|../*|file://*)
|
|
409
|
+
return 1
|
|
410
|
+
;;
|
|
411
|
+
esac
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
|
|
375
415
|
resolve_worktree_base_branch() {
|
|
376
416
|
local _wt="$1"
|
|
377
417
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
@@ -685,7 +725,12 @@ run_finish_flow() {
|
|
|
685
725
|
echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${GUARDEX_GH_BIN:-gh}" >&2
|
|
686
726
|
return 2
|
|
687
727
|
fi
|
|
688
|
-
|
|
728
|
+
if origin_remote_supports_pr_finish; then
|
|
729
|
+
finish_args+=(--via-pr)
|
|
730
|
+
else
|
|
731
|
+
echo "[codex-agent] Origin remote does not provide a mergeable PR surface; skipping auto-finish merge/PR pipeline." >&2
|
|
732
|
+
return 2
|
|
733
|
+
fi
|
|
689
734
|
else
|
|
690
735
|
echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
|
|
691
736
|
return 2
|
|
@@ -764,7 +809,9 @@ if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HE
|
|
|
764
809
|
else
|
|
765
810
|
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
|
|
766
811
|
fi
|
|
767
|
-
if
|
|
812
|
+
if ! auto_finish_context_is_ready "$worktree_path"; then
|
|
813
|
+
echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
|
|
814
|
+
elif auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
|
|
768
815
|
if run_finish_flow "$worktree_path" "$worktree_branch"; then
|
|
769
816
|
auto_finish_completed=1
|
|
770
817
|
echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
|