@imdeadpool/guardex 5.0.5 → 5.0.7
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 +13 -2
- package/bin/multiagent-safety.js +58 -1
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +2 -1
- package/templates/githooks/pre-commit +17 -1
- package/templates/githooks/pre-push +18 -3
- package/templates/scripts/agent-branch-finish.sh +1 -1
- package/templates/scripts/agent-worktree-prune.sh +103 -1
- package/templates/scripts/codex-agent.sh +75 -4
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ gx sync
|
|
|
110
110
|
# continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
|
|
111
111
|
bash scripts/review-bot-watch.sh --interval 30
|
|
112
112
|
|
|
113
|
-
# cleanup merged agent branches
|
|
113
|
+
# cleanup merged agent branches and hide clean stale agent worktrees
|
|
114
114
|
gx cleanup
|
|
115
115
|
|
|
116
116
|
# scan/report
|
|
@@ -143,7 +143,8 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
|
|
|
143
143
|
- `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
|
|
144
144
|
- Interactive self-update prompt defaults to **No** (`[y/N]`).
|
|
145
145
|
- In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
|
|
146
|
-
-
|
|
146
|
+
- Direct commits/pushes to protected branches are blocked by default (including VS Code Source Control).
|
|
147
|
+
- Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
|
|
147
148
|
- Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
|
|
148
149
|
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
|
|
149
150
|
- `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
|
|
@@ -238,6 +239,16 @@ npm pack --dry-run
|
|
|
238
239
|
|
|
239
240
|
## Release notes
|
|
240
241
|
|
|
242
|
+
### v5.0.7
|
|
243
|
+
|
|
244
|
+
- Bumped package version from `5.0.6` to `5.0.7` to stay one patch ahead for the next npm publish.
|
|
245
|
+
|
|
246
|
+
### v5.0.6
|
|
247
|
+
|
|
248
|
+
- `gx cleanup` and auto-finish cleanup now prune clean agent worktrees by default, so VS Code Source Control focuses on your local branch plus worktrees with active changes.
|
|
249
|
+
- Added `gx cleanup --keep-clean-worktrees` to opt out and keep clean worktrees visible.
|
|
250
|
+
- Bumped package version from `5.0.5` to `5.0.6` for the next npm publish.
|
|
251
|
+
|
|
241
252
|
### v5.0.5
|
|
242
253
|
|
|
243
254
|
- Bumped package version from `5.0.4` to `5.0.5` so npm publish can proceed with the next patch release.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -79,6 +79,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
79
79
|
|
|
80
80
|
const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
|
|
81
81
|
const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
|
|
82
|
+
const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
|
|
82
83
|
const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
83
84
|
const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
84
85
|
const MANAGED_GITIGNORE_PATHS = [
|
|
@@ -608,6 +609,10 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
608
609
|
function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
609
610
|
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
610
611
|
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
|
|
612
|
+
const managedRegex = new RegExp(
|
|
613
|
+
`${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
614
|
+
'm',
|
|
615
|
+
);
|
|
611
616
|
|
|
612
617
|
if (!fs.existsSync(agentsPath)) {
|
|
613
618
|
if (!dryRun) {
|
|
@@ -617,8 +622,19 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
|
617
622
|
}
|
|
618
623
|
|
|
619
624
|
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
625
|
+
if (managedRegex.test(existing)) {
|
|
626
|
+
const next = existing.replace(managedRegex, snippet);
|
|
627
|
+
if (next === existing) {
|
|
628
|
+
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
629
|
+
}
|
|
630
|
+
if (!dryRun) {
|
|
631
|
+
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
632
|
+
}
|
|
633
|
+
return { status: 'updated', file: 'AGENTS.md', note: 'refreshed guardex-managed block' };
|
|
634
|
+
}
|
|
635
|
+
|
|
620
636
|
if (existing.includes(AGENTS_MARKER_START)) {
|
|
621
|
-
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
637
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
|
|
622
638
|
}
|
|
623
639
|
|
|
624
640
|
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
@@ -1069,6 +1085,19 @@ function isCommandAvailable(commandName) {
|
|
|
1069
1085
|
return run('which', [commandName]).status === 0;
|
|
1070
1086
|
}
|
|
1071
1087
|
|
|
1088
|
+
function extractAgentBranchFinishPrUrl(output) {
|
|
1089
|
+
const match = String(output || '').match(/\[agent-branch-finish\] PR:\s*(\S+)/);
|
|
1090
|
+
return match ? match[1] : '';
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function doctorFinishFlowIsPending(output) {
|
|
1094
|
+
return (
|
|
1095
|
+
/\[agent-branch-finish\] PR merge not completed yet; leaving PR open\./.test(output) ||
|
|
1096
|
+
/\[agent-branch-finish\] Merge pending review\/check policy\. Branch cleanup skipped for now\./.test(output) ||
|
|
1097
|
+
/\[agent-branch-finish\] PR auto-merge enabled; waiting for required checks\/reviews\./.test(output)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1072
1101
|
function finishDoctorSandboxBranch(blocked, metadata) {
|
|
1073
1102
|
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1074
1103
|
if (!fs.existsSync(finishScript)) {
|
|
@@ -1122,6 +1151,17 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1122
1151
|
};
|
|
1123
1152
|
}
|
|
1124
1153
|
|
|
1154
|
+
const combinedOutput = `${finishResult.stdout || ''}\n${finishResult.stderr || ''}`;
|
|
1155
|
+
if (doctorFinishFlowIsPending(combinedOutput)) {
|
|
1156
|
+
return {
|
|
1157
|
+
status: 'pending',
|
|
1158
|
+
note: 'PR created and waiting for merge policy/checks',
|
|
1159
|
+
prUrl: extractAgentBranchFinishPrUrl(combinedOutput),
|
|
1160
|
+
stdout: finishResult.stdout || '',
|
|
1161
|
+
stderr: finishResult.stderr || '',
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1125
1165
|
return {
|
|
1126
1166
|
status: 'completed',
|
|
1127
1167
|
note: 'doctor sandbox finish flow completed',
|
|
@@ -1266,6 +1306,15 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1266
1306
|
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1267
1307
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1268
1308
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1309
|
+
} else if (finishResult.status === 'pending') {
|
|
1310
|
+
console.log(
|
|
1311
|
+
`[${TOOL_NAME}] Auto-finish pending for sandbox branch '${metadata.branch}': ${finishResult.note}.`,
|
|
1312
|
+
);
|
|
1313
|
+
if (finishResult.prUrl) {
|
|
1314
|
+
console.log(`[${TOOL_NAME}] PR: ${finishResult.prUrl}`);
|
|
1315
|
+
}
|
|
1316
|
+
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1317
|
+
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1269
1318
|
} else if (finishResult.status === 'failed') {
|
|
1270
1319
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
1271
1320
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
@@ -1818,6 +1867,7 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1818
1867
|
dryRun: false,
|
|
1819
1868
|
forceDirty: false,
|
|
1820
1869
|
keepRemote: false,
|
|
1870
|
+
keepCleanWorktrees: false,
|
|
1821
1871
|
};
|
|
1822
1872
|
|
|
1823
1873
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -1861,6 +1911,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1861
1911
|
options.keepRemote = true;
|
|
1862
1912
|
continue;
|
|
1863
1913
|
}
|
|
1914
|
+
if (arg === '--keep-clean-worktrees') {
|
|
1915
|
+
options.keepCleanWorktrees = true;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1864
1918
|
throw new Error(`Unknown option: ${arg}`);
|
|
1865
1919
|
}
|
|
1866
1920
|
|
|
@@ -2996,6 +3050,9 @@ function cleanup(rawArgs) {
|
|
|
2996
3050
|
if (options.dryRun) {
|
|
2997
3051
|
args.push('--dry-run');
|
|
2998
3052
|
}
|
|
3053
|
+
if (!options.keepCleanWorktrees) {
|
|
3054
|
+
args.push('--only-dirty-worktrees');
|
|
3055
|
+
}
|
|
2999
3056
|
args.push('--delete-branches');
|
|
3000
3057
|
if (!options.keepRemote) {
|
|
3001
3058
|
args.push('--delete-remote-branches');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.7",
|
|
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,
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"agent:safety:setup": "gx setup",
|
|
29
29
|
"agent:safety:scan": "gx scan",
|
|
30
30
|
"agent:safety:fix": "gx fix",
|
|
31
|
-
"agent:safety:doctor": "gx doctor"
|
|
31
|
+
"agent:safety:doctor": "gx doctor",
|
|
32
|
+
"agent:review:watch": "bash ./scripts/review-bot-watch.sh"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=18"
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
- 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.
|
|
18
18
|
- If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --via-pr` until merged.
|
|
19
19
|
- Completion is not valid until these are true: commit exists on the agent branch, branch is pushed to `origin`, and PR/merge status is produced by `agent-branch-finish.sh` or `codex-agent`.
|
|
20
|
-
-
|
|
20
|
+
- For every new task, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch; otherwise create a fresh one from the current local base snapshot with `scripts/agent-branch-start.sh`.
|
|
21
|
+
- Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
21
22
|
- If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
|
|
22
23
|
|
|
23
24
|
1. Explicit ownership before edits
|
|
@@ -28,6 +28,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
|
|
|
28
28
|
is_vscode_git_context=1
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
+
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
32
|
+
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
33
|
+
allow_vscode_protected_raw="false"
|
|
34
|
+
fi
|
|
35
|
+
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
36
|
+
|
|
37
|
+
allow_vscode_protected_branch_writes=0
|
|
38
|
+
case "$allow_vscode_protected" in
|
|
39
|
+
1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
|
|
40
|
+
0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
|
|
41
|
+
*) allow_vscode_protected_branch_writes=0 ;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
31
44
|
protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
32
45
|
if [[ -z "$protected_branches_raw" ]]; then
|
|
33
46
|
protected_branches_raw="dev main master"
|
|
@@ -111,7 +124,7 @@ MSG
|
|
|
111
124
|
fi
|
|
112
125
|
|
|
113
126
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
114
|
-
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
|
|
127
|
+
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
|
|
115
128
|
exit 0
|
|
116
129
|
fi
|
|
117
130
|
|
|
@@ -131,6 +144,9 @@ Use an agent branch first:
|
|
|
131
144
|
After finishing work:
|
|
132
145
|
bash scripts/agent-branch-finish.sh
|
|
133
146
|
|
|
147
|
+
Optional repo override for manual VS Code protected-branch commits:
|
|
148
|
+
git config multiagent.allowVscodeProtectedBranchWrites true
|
|
149
|
+
|
|
134
150
|
Temporary bypass (not recommended):
|
|
135
151
|
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
|
|
136
152
|
MSG
|
|
@@ -10,6 +10,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
|
|
|
10
10
|
is_vscode_git_context=1
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
+
allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
|
|
14
|
+
if [[ -z "$allow_vscode_protected_raw" ]]; then
|
|
15
|
+
allow_vscode_protected_raw="false"
|
|
16
|
+
fi
|
|
17
|
+
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
|
|
18
|
+
|
|
19
|
+
allow_vscode_protected_branch_writes=0
|
|
20
|
+
case "$allow_vscode_protected" in
|
|
21
|
+
1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
|
|
22
|
+
0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
|
|
23
|
+
*) allow_vscode_protected_branch_writes=0 ;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
13
26
|
is_codex_session=0
|
|
14
27
|
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
|
|
15
28
|
is_codex_session=1
|
|
@@ -56,14 +69,16 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
|
|
|
56
69
|
exit 1
|
|
57
70
|
fi
|
|
58
71
|
|
|
59
|
-
if [[ "$is_vscode_git_context" == "1" ]]; then
|
|
72
|
+
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
|
|
60
73
|
exit 0
|
|
61
74
|
fi
|
|
62
75
|
|
|
63
76
|
{
|
|
64
|
-
echo "[agent-branch-guard] Push to protected branch blocked
|
|
77
|
+
echo "[agent-branch-guard] Push to protected branch blocked."
|
|
65
78
|
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
|
|
66
|
-
echo "[agent-branch-guard] Use
|
|
79
|
+
echo "[agent-branch-guard] Use an agent branch and merge via PR."
|
|
80
|
+
echo "[agent-branch-guard] Optional VS Code override:"
|
|
81
|
+
echo " git config multiagent.allowVscodeProtectedBranchWrites true"
|
|
67
82
|
echo
|
|
68
83
|
echo "Temporary bypass (not recommended):"
|
|
69
84
|
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
|
|
@@ -545,7 +545,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
|
545
545
|
fi
|
|
546
546
|
|
|
547
547
|
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
548
|
-
prune_args=(--base "$BASE_BRANCH" --delete-branches)
|
|
548
|
+
prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
|
|
549
549
|
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
550
550
|
prune_args+=(--delete-remote-branches)
|
|
551
551
|
fi
|
|
@@ -7,6 +7,7 @@ DRY_RUN=0
|
|
|
7
7
|
FORCE_DIRTY=0
|
|
8
8
|
DELETE_BRANCHES=0
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
|
+
ONLY_DIRTY_WORKTREES=0
|
|
10
11
|
TARGET_BRANCH=""
|
|
11
12
|
|
|
12
13
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
@@ -36,13 +37,17 @@ while [[ $# -gt 0 ]]; do
|
|
|
36
37
|
DELETE_REMOTE_BRANCHES=1
|
|
37
38
|
shift
|
|
38
39
|
;;
|
|
40
|
+
--only-dirty-worktrees)
|
|
41
|
+
ONLY_DIRTY_WORKTREES=1
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
39
44
|
--branch)
|
|
40
45
|
TARGET_BRANCH="${2:-}"
|
|
41
46
|
shift 2
|
|
42
47
|
;;
|
|
43
48
|
*)
|
|
44
49
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
45
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
|
|
50
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
|
|
46
51
|
exit 1
|
|
47
52
|
;;
|
|
48
53
|
esac
|
|
@@ -56,6 +61,11 @@ fi
|
|
|
56
61
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
57
62
|
current_pwd="$(pwd -P)"
|
|
58
63
|
worktree_root="${repo_root}/.omx/agent-worktrees"
|
|
64
|
+
repo_common_dir="$(
|
|
65
|
+
git -C "$repo_root" rev-parse --git-common-dir \
|
|
66
|
+
| awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
|
|
67
|
+
)"
|
|
68
|
+
repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
|
|
59
69
|
|
|
60
70
|
resolve_base_branch() {
|
|
61
71
|
local configured=""
|
|
@@ -127,11 +137,98 @@ is_clean_worktree() {
|
|
|
127
137
|
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
resolve_worktree_common_dir() {
|
|
141
|
+
local wt="$1"
|
|
142
|
+
local common_dir=""
|
|
143
|
+
common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null || true)"
|
|
144
|
+
if [[ -z "$common_dir" ]]; then
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
if [[ "$common_dir" == /* ]]; then
|
|
148
|
+
common_dir="$(cd "$common_dir" 2>/dev/null && pwd -P || true)"
|
|
149
|
+
else
|
|
150
|
+
common_dir="$(cd "$wt/$common_dir" 2>/dev/null && pwd -P || true)"
|
|
151
|
+
fi
|
|
152
|
+
if [[ -z "$common_dir" ]]; then
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
printf '%s' "$common_dir"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
select_unique_worktree_path() {
|
|
159
|
+
local root="$1"
|
|
160
|
+
local name="$2"
|
|
161
|
+
local candidate="${root}/${name}"
|
|
162
|
+
local suffix=2
|
|
163
|
+
while [[ -e "$candidate" ]]; do
|
|
164
|
+
candidate="${root}/${name}-${suffix}"
|
|
165
|
+
suffix=$((suffix + 1))
|
|
166
|
+
done
|
|
167
|
+
printf '%s' "$candidate"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
relocated_foreign=0
|
|
171
|
+
skipped_foreign=0
|
|
172
|
+
|
|
173
|
+
relocate_foreign_worktree_entries() {
|
|
174
|
+
[[ -d "$worktree_root" ]] || return 0
|
|
175
|
+
|
|
176
|
+
local entry=""
|
|
177
|
+
for entry in "${worktree_root}"/*; do
|
|
178
|
+
[[ -d "$entry" ]] || continue
|
|
179
|
+
if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
180
|
+
continue
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
local entry_common_dir=""
|
|
184
|
+
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
|
|
185
|
+
[[ -n "$entry_common_dir" ]] || continue
|
|
186
|
+
|
|
187
|
+
if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
|
|
192
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
193
|
+
echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
|
|
194
|
+
continue
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
local owner_repo_root
|
|
198
|
+
owner_repo_root="$(dirname "$entry_common_dir")"
|
|
199
|
+
local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
|
|
200
|
+
local target_path
|
|
201
|
+
target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
|
|
202
|
+
|
|
203
|
+
if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
|
|
204
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
205
|
+
echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
|
|
206
|
+
continue
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
|
|
210
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
211
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
212
|
+
continue
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
mkdir -p "$owner_worktree_root"
|
|
216
|
+
if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
|
|
217
|
+
relocated_foreign=$((relocated_foreign + 1))
|
|
218
|
+
else
|
|
219
|
+
skipped_foreign=$((skipped_foreign + 1))
|
|
220
|
+
echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
|
|
221
|
+
fi
|
|
222
|
+
done
|
|
223
|
+
}
|
|
224
|
+
|
|
130
225
|
removed_worktrees=0
|
|
131
226
|
removed_branches=0
|
|
132
227
|
skipped_active=0
|
|
133
228
|
skipped_dirty=0
|
|
134
229
|
|
|
230
|
+
relocate_foreign_worktree_entries
|
|
231
|
+
|
|
135
232
|
process_entry() {
|
|
136
233
|
local wt="$1"
|
|
137
234
|
local branch_ref="$2"
|
|
@@ -165,6 +262,8 @@ process_entry() {
|
|
|
165
262
|
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
166
263
|
remove_reason="merged-agent-branch"
|
|
167
264
|
fi
|
|
265
|
+
elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
|
|
266
|
+
remove_reason="clean-agent-worktree"
|
|
168
267
|
fi
|
|
169
268
|
elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
|
|
170
269
|
remove_reason="temporary-worktree"
|
|
@@ -258,6 +357,9 @@ fi
|
|
|
258
357
|
run_cmd git -C "$repo_root" worktree prune
|
|
259
358
|
|
|
260
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}"
|
|
360
|
+
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
|
|
361
|
+
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
|
|
362
|
+
fi
|
|
261
363
|
if [[ "$skipped_active" -gt 0 ]]; then
|
|
262
364
|
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
|
|
263
365
|
fi
|
|
@@ -285,13 +285,84 @@ auto_commit_worktree_changes() {
|
|
|
285
285
|
|
|
286
286
|
local default_message="Auto-finish: ${TASK_NAME}"
|
|
287
287
|
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
|
|
288
|
+
local commit_output=""
|
|
288
289
|
|
|
289
|
-
if
|
|
290
|
-
echo "[codex-agent] Auto-
|
|
290
|
+
if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
|
|
291
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
|
|
292
|
+
return 0
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
if auto_sync_for_commit_retry "$wt" "$branch"; then
|
|
296
|
+
claim_changed_files "$wt" "$branch"
|
|
297
|
+
git -C "$wt" add -A
|
|
298
|
+
if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
|
|
299
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}' after sync retry."
|
|
300
|
+
return 0
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
|
|
305
|
+
if [[ -n "$commit_output" ]]; then
|
|
306
|
+
printf '%s\n' "$commit_output" >&2
|
|
307
|
+
fi
|
|
308
|
+
return 1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
auto_sync_for_commit_retry() {
|
|
312
|
+
local wt="$1"
|
|
313
|
+
local branch="$2"
|
|
314
|
+
|
|
315
|
+
if ! has_origin_remote; then
|
|
316
|
+
return 1
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
local base_branch
|
|
320
|
+
base_branch="$(resolve_worktree_base_branch "$wt")"
|
|
321
|
+
if [[ -z "$base_branch" ]]; then
|
|
291
322
|
return 1
|
|
292
323
|
fi
|
|
293
324
|
|
|
294
|
-
|
|
325
|
+
if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
|
|
326
|
+
return 1
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
330
|
+
return 1
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
local behind_count
|
|
334
|
+
behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
|
|
335
|
+
behind_count="${behind_count:-0}"
|
|
336
|
+
if [[ "$behind_count" -le 0 ]]; then
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
echo "[codex-agent] Auto-commit retry: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Syncing and retrying..."
|
|
341
|
+
|
|
342
|
+
local stash_ref=""
|
|
343
|
+
local stash_output=""
|
|
344
|
+
if worktree_has_changes "$wt"; then
|
|
345
|
+
if ! stash_output="$(git -C "$wt" stash push --include-untracked -m "codex-agent-autocommit-sync-${branch}-$(date +%s)" 2>&1)"; then
|
|
346
|
+
return 1
|
|
347
|
+
fi
|
|
348
|
+
stash_ref="$(printf '%s\n' "$stash_output" | grep -o 'stash@{[0-9]\+}' | head -n 1 || true)"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
if ! git -C "$wt" rebase "origin/${base_branch}" >/dev/null 2>&1; then
|
|
352
|
+
git -C "$wt" rebase --abort >/dev/null 2>&1 || true
|
|
353
|
+
if [[ -n "$stash_ref" ]]; then
|
|
354
|
+
git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1 || true
|
|
355
|
+
fi
|
|
356
|
+
return 1
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
if [[ -n "$stash_ref" ]]; then
|
|
360
|
+
if ! git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1; then
|
|
361
|
+
echo "[codex-agent] Auto-commit retry could not re-apply local changes after sync. Manual resolution required in: $wt" >&2
|
|
362
|
+
return 1
|
|
363
|
+
fi
|
|
364
|
+
fi
|
|
365
|
+
|
|
295
366
|
return 0
|
|
296
367
|
}
|
|
297
368
|
|
|
@@ -419,7 +490,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
|
419
490
|
prune_args+=(--base "$BASE_BRANCH")
|
|
420
491
|
fi
|
|
421
492
|
if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
|
|
422
|
-
prune_args+=(--delete-branches --delete-remote-branches)
|
|
493
|
+
prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches)
|
|
423
494
|
fi
|
|
424
495
|
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
425
496
|
echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
|