@imdeadpool/guardex 5.0.1 → 5.0.2
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 +17 -5
- package/bin/multiagent-safety.js +243 -9
- package/package.json +1 -1
- package/templates/AGENTS.multiagent-safety.md +5 -1
- package/templates/codex/skills/guardex/SKILL.md +2 -0
- package/templates/githooks/pre-commit +22 -0
- package/templates/scripts/agent-branch-finish.sh +81 -31
- package/templates/scripts/agent-worktree-prune.sh +57 -14
- package/templates/scripts/codex-agent.sh +301 -2
package/README.md
CHANGED
|
@@ -238,6 +238,10 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
|
|
|
238
238
|
- For every new user message/task, repeat the same cycle:
|
|
239
239
|
start isolated agent branch/worktree -> claim file locks -> implement/verify ->
|
|
240
240
|
finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
|
|
241
|
+
- `scripts/codex-agent.sh` now auto-runs this finish flow after Codex exits:
|
|
242
|
+
auto-commit changed files -> push/create PR -> merge attempt -> keep branch/worktree for follow-up.
|
|
243
|
+
- Remove merged branches when you are done reviewing:
|
|
244
|
+
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
241
245
|
|
|
242
246
|
5) Optional: create OpenSpec planning workspace:
|
|
243
247
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -269,9 +273,11 @@ gx protect set <branch...> [--target <path>]
|
|
|
269
273
|
gx protect reset [--target <path>]
|
|
270
274
|
gx sync --check [--target <path>] [--base <branch>] [--json]
|
|
271
275
|
gx sync [--target <path>] [--base <branch>] [--strategy rebase|merge] [--ff-only]
|
|
276
|
+
gx cleanup [--target <path>] [--base <branch>] [--branch <agent/...>] [--dry-run] [--force-dirty] [--keep-remote]
|
|
272
277
|
gx report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD]
|
|
273
|
-
bash scripts/agent-worktree-prune.sh #
|
|
274
|
-
bash scripts/agent-worktree-prune.sh --
|
|
278
|
+
bash scripts/agent-worktree-prune.sh # prune temporary worktrees only (keeps merged agent branches by default)
|
|
279
|
+
bash scripts/agent-worktree-prune.sh --delete-branches --delete-remote-branches # full merged-branch cleanup
|
|
280
|
+
bash scripts/agent-worktree-prune.sh --force-dirty --delete-branches # force-remove dirty merged worktrees too
|
|
275
281
|
bash scripts/openspec/init-plan-workspace.sh <plan-slug> # optional OpenSpec plan scaffold
|
|
276
282
|
```
|
|
277
283
|
|
|
@@ -284,8 +290,14 @@ and asks `[y/N]` whether to update immediately (default is `N`).
|
|
|
284
290
|
- Interactive setup: prompts for Y/N approval before global OMX/OpenSpec/codex-auth install.
|
|
285
291
|
- Interactive prompt is strict (`[y/n]`) and waits for explicit answer.
|
|
286
292
|
- Non-interactive setup: skips global installs by default; use `--yes-global-install` to force.
|
|
287
|
-
- In already-initialized repos, `setup` / `install` / `fix`
|
|
288
|
-
- `
|
|
293
|
+
- In already-initialized repos, `setup` / `install` / `fix` block writes on protected `main` by default; start an agent branch first. Use `--allow-protected-base-write` only for emergency in-place maintenance.
|
|
294
|
+
- `gx doctor` on protected `main` auto-starts an isolated `agent/gx/...-gx-doctor` worktree branch and applies repairs there.
|
|
295
|
+
- `gx setup` and `gx doctor` always refresh `.githooks/pre-commit` from templates, so Codex sub-branch enforcement stays repaired.
|
|
296
|
+
- `scripts/codex-agent.sh` now auto-runs finish automation after a Codex session when `origin` exists:
|
|
297
|
+
auto-commit changed files, run PR/merge automation, and keep merged agent branches/worktrees by default.
|
|
298
|
+
It also auto-syncs each sandbox branch against the latest base branch before task execution.
|
|
299
|
+
If conflicts remain, it keeps the sandbox and prompts for a conflict-resolution review pass.
|
|
300
|
+
- use `gx cleanup` (or `gx cleanup --branch <agent/...>`) to remove merged branches/worktrees when done.
|
|
289
301
|
|
|
290
302
|
## Advanced commands
|
|
291
303
|
|
|
@@ -362,7 +374,7 @@ multiagent.protectedBranches
|
|
|
362
374
|
## What is protected
|
|
363
375
|
|
|
364
376
|
- direct commits to protected branches (defaults: `dev`, `main`, `master`; configurable via `gx protect ...`)
|
|
365
|
-
- protected-branch commits are blocked
|
|
377
|
+
- protected-branch commits are blocked by default for all clients; Codex sessions only may commit protected branches when staged files are strictly `AGENTS.md` and/or `.gitignore`
|
|
366
378
|
- Codex-session commits on non-`agent/*` branches are blocked by default (`multiagent.codexRequireAgentBranch=true`)
|
|
367
379
|
- Codex commits attempted on protected branches trigger `guardex-preedit-guard` and require starting work via `scripts/codex-agent.sh`
|
|
368
380
|
- overlapping file ownership between agents
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -58,6 +58,8 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
58
58
|
'.githooks/pre-commit',
|
|
59
59
|
'scripts/agent-branch-start.sh',
|
|
60
60
|
'scripts/agent-branch-finish.sh',
|
|
61
|
+
'scripts/agent-worktree-prune.sh',
|
|
62
|
+
'scripts/codex-agent.sh',
|
|
61
63
|
'scripts/agent-file-locks.py',
|
|
62
64
|
]);
|
|
63
65
|
|
|
@@ -88,6 +90,7 @@ const COMMAND_TYPO_ALIASES = new Map([
|
|
|
88
90
|
['intsall', 'install'],
|
|
89
91
|
['docter', 'doctor'],
|
|
90
92
|
['doctro', 'doctor'],
|
|
93
|
+
['cleunup', 'cleanup'],
|
|
91
94
|
['scna', 'scan'],
|
|
92
95
|
]);
|
|
93
96
|
const SUGGESTIBLE_COMMANDS = [
|
|
@@ -100,6 +103,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
100
103
|
'copy-commands',
|
|
101
104
|
'protect',
|
|
102
105
|
'sync',
|
|
106
|
+
'cleanup',
|
|
103
107
|
'release',
|
|
104
108
|
'install',
|
|
105
109
|
'fix',
|
|
@@ -118,6 +122,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
118
122
|
['copy-commands', 'Print setup checklist as executable commands only'],
|
|
119
123
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
120
124
|
['sync', 'Check or sync agent branches with origin/<base>'],
|
|
125
|
+
['cleanup', 'Cleanup merged agent branches/worktrees (local + remote)'],
|
|
121
126
|
['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
|
|
122
127
|
['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
|
|
123
128
|
['scan', 'Report safety issues and exit non-zero on findings'],
|
|
@@ -152,6 +157,9 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
152
157
|
- For every new user message/task, repeat the same cycle:
|
|
153
158
|
start isolated agent branch/worktree -> claim file locks -> implement/verify ->
|
|
154
159
|
finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
|
|
160
|
+
- Finished branches stay available by default for audit/follow-up.
|
|
161
|
+
Remove them explicitly when done:
|
|
162
|
+
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
155
163
|
|
|
156
164
|
5) Optional: create OpenSpec planning workspace:
|
|
157
165
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
@@ -174,6 +182,7 @@ bash scripts/codex-agent.sh "task" "agent-name"
|
|
|
174
182
|
bash scripts/agent-branch-start.sh "task" "agent-name"
|
|
175
183
|
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
|
|
176
184
|
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
185
|
+
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
177
186
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
178
187
|
gx protect add release staging
|
|
179
188
|
gx sync --check
|
|
@@ -288,7 +297,10 @@ NOTES
|
|
|
288
297
|
- Short alias: ${SHORT_TOOL_NAME}
|
|
289
298
|
- ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
|
|
290
299
|
- ${TOOL_NAME} setup asks for Y/N approval before global installs
|
|
291
|
-
- In initialized repos, setup/install/fix
|
|
300
|
+
- In initialized repos, setup/install/fix block in-place writes on protected main by default
|
|
301
|
+
- doctor auto-starts a sandbox agent branch/worktree when run on protected main
|
|
302
|
+
- agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
|
|
303
|
+
- use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
|
|
292
304
|
- Legacy command aliases are still supported: ${LEGACY_NAMES.join(', ')}`);
|
|
293
305
|
|
|
294
306
|
if (outsideGitRepo) {
|
|
@@ -362,6 +374,10 @@ function ensureExecutable(destinationPath, relativePath, dryRun) {
|
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
376
|
|
|
377
|
+
function isCriticalGuardrailPath(relativePath) {
|
|
378
|
+
return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
|
|
379
|
+
}
|
|
380
|
+
|
|
365
381
|
function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
366
382
|
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
|
|
367
383
|
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
|
|
@@ -376,7 +392,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
|
376
392
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
377
393
|
return { status: 'unchanged', file: destinationRelativePath };
|
|
378
394
|
}
|
|
379
|
-
if (!force) {
|
|
395
|
+
if (!force && !isCriticalGuardrailPath(destinationRelativePath)) {
|
|
380
396
|
throw new Error(
|
|
381
397
|
`Refusing to overwrite existing file without --force: ${destinationRelativePath}`,
|
|
382
398
|
);
|
|
@@ -389,6 +405,10 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
|
389
405
|
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
390
406
|
}
|
|
391
407
|
|
|
408
|
+
if (destinationExists && !force && isCriticalGuardrailPath(destinationRelativePath)) {
|
|
409
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
|
|
410
|
+
}
|
|
411
|
+
|
|
392
412
|
return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
|
|
393
413
|
}
|
|
394
414
|
|
|
@@ -405,6 +425,14 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
|
|
|
405
425
|
return { status: 'unchanged', file: destinationRelativePath };
|
|
406
426
|
}
|
|
407
427
|
|
|
428
|
+
if (isCriticalGuardrailPath(destinationRelativePath)) {
|
|
429
|
+
if (!dryRun) {
|
|
430
|
+
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
431
|
+
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
432
|
+
}
|
|
433
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
|
|
434
|
+
}
|
|
435
|
+
|
|
408
436
|
// In fix mode, avoid silently replacing local customizations.
|
|
409
437
|
return { status: 'skipped-conflict', file: destinationRelativePath };
|
|
410
438
|
}
|
|
@@ -489,7 +517,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
489
517
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
490
518
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
491
519
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
492
|
-
'agent:cleanup':
|
|
520
|
+
'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
|
|
493
521
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
494
522
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
495
523
|
'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
|
|
@@ -671,34 +699,139 @@ function hasGuardexBootstrapFiles(repoRoot) {
|
|
|
671
699
|
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
672
700
|
}
|
|
673
701
|
|
|
674
|
-
function
|
|
702
|
+
function protectedBaseWriteBlock(options) {
|
|
675
703
|
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
676
|
-
return;
|
|
704
|
+
return null;
|
|
677
705
|
}
|
|
678
706
|
|
|
679
707
|
const repoRoot = resolveRepoRoot(options.target);
|
|
680
708
|
if (!hasGuardexBootstrapFiles(repoRoot)) {
|
|
681
|
-
return;
|
|
709
|
+
return null;
|
|
682
710
|
}
|
|
683
711
|
|
|
684
712
|
const branch = currentBranchName(repoRoot);
|
|
685
713
|
if (branch !== 'main') {
|
|
686
|
-
return;
|
|
714
|
+
return null;
|
|
687
715
|
}
|
|
688
716
|
|
|
689
717
|
const protectedBranches = readProtectedBranches(repoRoot);
|
|
690
718
|
if (!protectedBranches.includes(branch)) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
repoRoot,
|
|
724
|
+
branch,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function assertProtectedMainWriteAllowed(options, commandName) {
|
|
729
|
+
const blocked = protectedBaseWriteBlock(options);
|
|
730
|
+
if (!blocked) {
|
|
691
731
|
return;
|
|
692
732
|
}
|
|
693
733
|
|
|
694
734
|
throw new Error(
|
|
695
|
-
`${commandName} blocked on protected branch '${branch}' in an initialized repo.\n` +
|
|
696
|
-
`Keep local '${branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
735
|
+
`${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
|
|
736
|
+
`Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
697
737
|
` bash scripts/agent-branch-start.sh "<task>" "codex"\n` +
|
|
698
738
|
`Override once only when intentional: --allow-protected-base-write`,
|
|
699
739
|
);
|
|
700
740
|
}
|
|
701
741
|
|
|
742
|
+
function extractAgentBranchStartMetadata(output) {
|
|
743
|
+
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
|
|
744
|
+
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
745
|
+
return {
|
|
746
|
+
branch: branchMatch ? branchMatch[1].trim() : '',
|
|
747
|
+
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
752
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
753
|
+
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
754
|
+
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
755
|
+
throw new Error(`doctor target must stay inside repo root when sandboxing: ${resolvedTarget}`);
|
|
756
|
+
}
|
|
757
|
+
if (!relativeTarget || relativeTarget === '.') {
|
|
758
|
+
return worktreePath;
|
|
759
|
+
}
|
|
760
|
+
return path.join(worktreePath, relativeTarget);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
764
|
+
const args = ['doctor', '--target', sandboxTarget];
|
|
765
|
+
if (options.dryRun) args.push('--dry-run');
|
|
766
|
+
if (options.skipAgents) args.push('--skip-agents');
|
|
767
|
+
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
768
|
+
if (options.skipGitignore) args.push('--no-gitignore');
|
|
769
|
+
if (!options.dropStaleLocks) args.push('--keep-stale-locks');
|
|
770
|
+
if (options.json) args.push('--json');
|
|
771
|
+
return args;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function runDoctorInSandbox(options, blocked) {
|
|
775
|
+
const startScript = path.join(blocked.repoRoot, 'scripts', 'agent-branch-start.sh');
|
|
776
|
+
if (!fs.existsSync(startScript)) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`doctor sandbox fallback is unavailable because '${startScript}' is missing.\n` +
|
|
779
|
+
`Run '${SHORT_TOOL_NAME} setup --allow-protected-base-write' once to restore branch-start tooling.`,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const startResult = run('bash', [
|
|
784
|
+
startScript,
|
|
785
|
+
'--task',
|
|
786
|
+
`${SHORT_TOOL_NAME}-doctor`,
|
|
787
|
+
'--agent',
|
|
788
|
+
SHORT_TOOL_NAME,
|
|
789
|
+
'--base',
|
|
790
|
+
blocked.branch,
|
|
791
|
+
], { cwd: blocked.repoRoot });
|
|
792
|
+
if (startResult.error) {
|
|
793
|
+
throw startResult.error;
|
|
794
|
+
}
|
|
795
|
+
if (startResult.status !== 0) {
|
|
796
|
+
throw new Error((startResult.stderr || startResult.stdout || 'failed to start doctor sandbox').trim());
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
800
|
+
if (!metadata.worktreePath) {
|
|
801
|
+
throw new Error(`Failed to parse sandbox worktree from agent-branch-start output:\n${startResult.stdout}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
805
|
+
const nestedResult = run(
|
|
806
|
+
process.execPath,
|
|
807
|
+
[__filename, ...buildSandboxDoctorArgs(options, sandboxTarget)],
|
|
808
|
+
{ cwd: metadata.worktreePath },
|
|
809
|
+
);
|
|
810
|
+
if (nestedResult.error) {
|
|
811
|
+
throw nestedResult.error;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (options.json) {
|
|
815
|
+
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
816
|
+
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
817
|
+
} else {
|
|
818
|
+
console.log(
|
|
819
|
+
`[${TOOL_NAME}] doctor detected protected branch '${blocked.branch}'. ` +
|
|
820
|
+
`Running repairs in sandbox branch '${metadata.branch || 'agent/<auto>'}'.`,
|
|
821
|
+
);
|
|
822
|
+
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
823
|
+
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
824
|
+
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
825
|
+
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (typeof nestedResult.status === 'number') {
|
|
829
|
+
process.exitCode = nestedResult.status;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
process.exitCode = 1;
|
|
833
|
+
}
|
|
834
|
+
|
|
702
835
|
function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
|
|
703
836
|
const remaining = [];
|
|
704
837
|
let target = defaultTarget;
|
|
@@ -1134,6 +1267,63 @@ function parseSyncArgs(rawArgs) {
|
|
|
1134
1267
|
return options;
|
|
1135
1268
|
}
|
|
1136
1269
|
|
|
1270
|
+
function parseCleanupArgs(rawArgs) {
|
|
1271
|
+
const options = {
|
|
1272
|
+
target: process.cwd(),
|
|
1273
|
+
base: '',
|
|
1274
|
+
branch: '',
|
|
1275
|
+
dryRun: false,
|
|
1276
|
+
forceDirty: false,
|
|
1277
|
+
keepRemote: false,
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1281
|
+
const arg = rawArgs[index];
|
|
1282
|
+
if (arg === '--target') {
|
|
1283
|
+
const next = rawArgs[index + 1];
|
|
1284
|
+
if (!next) {
|
|
1285
|
+
throw new Error('--target requires a path value');
|
|
1286
|
+
}
|
|
1287
|
+
options.target = next;
|
|
1288
|
+
index += 1;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (arg === '--base') {
|
|
1292
|
+
const next = rawArgs[index + 1];
|
|
1293
|
+
if (!next) {
|
|
1294
|
+
throw new Error('--base requires a branch value');
|
|
1295
|
+
}
|
|
1296
|
+
options.base = next;
|
|
1297
|
+
index += 1;
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (arg === '--branch') {
|
|
1301
|
+
const next = rawArgs[index + 1];
|
|
1302
|
+
if (!next) {
|
|
1303
|
+
throw new Error('--branch requires an agent branch value');
|
|
1304
|
+
}
|
|
1305
|
+
options.branch = next;
|
|
1306
|
+
index += 1;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (arg === '--dry-run') {
|
|
1310
|
+
options.dryRun = true;
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (arg === '--force-dirty') {
|
|
1314
|
+
options.forceDirty = true;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (arg === '--keep-remote') {
|
|
1318
|
+
options.keepRemote = true;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return options;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1137
1327
|
function syncOperation(repoRoot, strategy, baseRef, ffOnly) {
|
|
1138
1328
|
if (strategy === 'rebase') {
|
|
1139
1329
|
if (ffOnly) {
|
|
@@ -1894,6 +2084,12 @@ function doctor(rawArgs) {
|
|
|
1894
2084
|
allowProtectedBaseWrite: false,
|
|
1895
2085
|
});
|
|
1896
2086
|
|
|
2087
|
+
const blocked = protectedBaseWriteBlock(options);
|
|
2088
|
+
if (blocked) {
|
|
2089
|
+
runDoctorInSandbox(options, blocked);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1897
2093
|
assertProtectedMainWriteAllowed(options, 'doctor');
|
|
1898
2094
|
const fixPayload = runFixInternal(options);
|
|
1899
2095
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
@@ -2156,6 +2352,39 @@ function copyCommands() {
|
|
|
2156
2352
|
process.exitCode = 0;
|
|
2157
2353
|
}
|
|
2158
2354
|
|
|
2355
|
+
function cleanup(rawArgs) {
|
|
2356
|
+
const options = parseCleanupArgs(rawArgs);
|
|
2357
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
2358
|
+
const pruneScript = path.join(repoRoot, 'scripts', 'agent-worktree-prune.sh');
|
|
2359
|
+
if (!fs.existsSync(pruneScript)) {
|
|
2360
|
+
throw new Error(`Missing cleanup script: ${pruneScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
const args = [pruneScript];
|
|
2364
|
+
if (options.base) {
|
|
2365
|
+
args.push('--base', options.base);
|
|
2366
|
+
}
|
|
2367
|
+
if (options.branch) {
|
|
2368
|
+
args.push('--branch', options.branch);
|
|
2369
|
+
}
|
|
2370
|
+
if (options.forceDirty) {
|
|
2371
|
+
args.push('--force-dirty');
|
|
2372
|
+
}
|
|
2373
|
+
if (options.dryRun) {
|
|
2374
|
+
args.push('--dry-run');
|
|
2375
|
+
}
|
|
2376
|
+
args.push('--delete-branches');
|
|
2377
|
+
if (!options.keepRemote) {
|
|
2378
|
+
args.push('--delete-remote-branches');
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const runResult = run('bash', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
2382
|
+
if (runResult.status !== 0) {
|
|
2383
|
+
throw new Error('Cleanup command failed');
|
|
2384
|
+
}
|
|
2385
|
+
process.exitCode = 0;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2159
2388
|
function sync(rawArgs) {
|
|
2160
2389
|
const options = parseSyncArgs(rawArgs);
|
|
2161
2390
|
const repoRoot = resolveRepoRoot(options.target);
|
|
@@ -2504,6 +2733,11 @@ function main() {
|
|
|
2504
2733
|
return;
|
|
2505
2734
|
}
|
|
2506
2735
|
|
|
2736
|
+
if (command === 'cleanup') {
|
|
2737
|
+
cleanup(rest);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2507
2741
|
if (command === 'release') {
|
|
2508
2742
|
release(rest);
|
|
2509
2743
|
return;
|
package/package.json
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
- If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
|
|
12
12
|
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
|
|
13
13
|
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
|
|
14
|
-
- Agent completion
|
|
14
|
+
- 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).
|
|
15
|
+
- Auto-finish keeps the sandbox branch/worktree by default so conflict follow-ups and audits stay reproducible.
|
|
16
|
+
- Use explicit cleanup when done: `gx cleanup --branch "<agent-branch>"` (or `gx cleanup` for all merged agent branches).
|
|
17
|
+
- If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr` and keep the branch open until checks/review pass.
|
|
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.
|
|
15
19
|
- Per-message loop is mandatory: for every new user message/task, start a fresh agent branch/worktree, claim ownership locks, implement and verify, finish via PR/merge cleanup, then repeat for the next message/task.
|
|
16
20
|
|
|
17
21
|
1. Explicit ownership before edits
|
|
@@ -36,4 +36,6 @@ gx scan
|
|
|
36
36
|
- Keep agent work isolated (`agent/*` branches + lock claims).
|
|
37
37
|
- For every new user message/task, restart the full loop on a fresh agent branch/worktree.
|
|
38
38
|
- For one-command Codex sandbox startup, use `bash scripts/codex-agent.sh "<task>" "<agent-name>"`.
|
|
39
|
+
- `scripts/codex-agent.sh` auto-syncs the sandbox branch against base before each task and auto-finishes merge/PR flow after Codex exits.
|
|
40
|
+
- Auto-finish keeps the branch/worktree by default; remove merged branches explicitly with `gx cleanup` (or `gx cleanup --branch "<agent-branch>"`).
|
|
39
41
|
- Do not bypass protected branch safeguards unless explicitly required.
|
|
@@ -50,9 +50,31 @@ case "$codex_require_agent_branch" in
|
|
|
50
50
|
*) should_require_codex_agent_branch=1 ;;
|
|
51
51
|
esac
|
|
52
52
|
|
|
53
|
+
is_codex_managed_only_commit_on_protected=0
|
|
54
|
+
if [[ "$is_codex_session" == "1" && "$is_protected_branch" == "1" ]]; then
|
|
55
|
+
deleted_paths="$(git diff --cached --name-only --diff-filter=D)"
|
|
56
|
+
staged_paths="$(git diff --cached --name-only --diff-filter=ACMRTUXB)"
|
|
57
|
+
if [[ -z "$deleted_paths" && -n "$staged_paths" ]]; then
|
|
58
|
+
managed_only=1
|
|
59
|
+
while IFS= read -r staged_path; do
|
|
60
|
+
case "$staged_path" in
|
|
61
|
+
AGENTS.md|.gitignore) ;;
|
|
62
|
+
*) managed_only=0; break ;;
|
|
63
|
+
esac
|
|
64
|
+
done <<< "$staged_paths"
|
|
65
|
+
if [[ "$managed_only" == "1" ]]; then
|
|
66
|
+
is_codex_managed_only_commit_on_protected=1
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
53
71
|
if [[ "$should_require_codex_agent_branch" == "1" && "${MUSAFETY_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
|
|
54
72
|
if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
|
|
55
73
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
74
|
+
if [[ "$is_codex_managed_only_commit_on_protected" == "1" ]]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
56
78
|
cat >&2 <<'MSG'
|
|
57
79
|
[guardex-preedit-guard] Codex edit/commit detected on a protected branch.
|
|
58
80
|
GuardeX requires Codex work to run from an isolated agent/* branch.
|
|
@@ -5,9 +5,26 @@ BASE_BRANCH=""
|
|
|
5
5
|
BASE_BRANCH_EXPLICIT=0
|
|
6
6
|
SOURCE_BRANCH=""
|
|
7
7
|
PUSH_ENABLED=1
|
|
8
|
-
DELETE_REMOTE_BRANCH=
|
|
8
|
+
DELETE_REMOTE_BRANCH=0
|
|
9
|
+
DELETE_REMOTE_BRANCH_EXPLICIT=0
|
|
9
10
|
MERGE_MODE="auto"
|
|
10
11
|
GH_BIN="${MUSAFETY_GH_BIN:-gh}"
|
|
12
|
+
CLEANUP_AFTER_MERGE_RAW="${MUSAFETY_FINISH_CLEANUP:-false}"
|
|
13
|
+
|
|
14
|
+
normalize_bool() {
|
|
15
|
+
local raw="${1:-}"
|
|
16
|
+
local fallback="${2:-0}"
|
|
17
|
+
local lowered
|
|
18
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
19
|
+
case "$lowered" in
|
|
20
|
+
1|true|yes|on) printf '1' ;;
|
|
21
|
+
0|false|no|off) printf '0' ;;
|
|
22
|
+
'') printf '%s' "$fallback" ;;
|
|
23
|
+
*) printf '%s' "$fallback" ;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
|
|
11
28
|
|
|
12
29
|
while [[ $# -gt 0 ]]; do
|
|
13
30
|
case "$1" in
|
|
@@ -26,6 +43,20 @@ while [[ $# -gt 0 ]]; do
|
|
|
26
43
|
;;
|
|
27
44
|
--keep-remote-branch)
|
|
28
45
|
DELETE_REMOTE_BRANCH=0
|
|
46
|
+
DELETE_REMOTE_BRANCH_EXPLICIT=1
|
|
47
|
+
shift
|
|
48
|
+
;;
|
|
49
|
+
--delete-remote-branch)
|
|
50
|
+
DELETE_REMOTE_BRANCH=1
|
|
51
|
+
DELETE_REMOTE_BRANCH_EXPLICIT=1
|
|
52
|
+
shift
|
|
53
|
+
;;
|
|
54
|
+
--cleanup)
|
|
55
|
+
CLEANUP_AFTER_MERGE=1
|
|
56
|
+
shift
|
|
57
|
+
;;
|
|
58
|
+
--no-cleanup)
|
|
59
|
+
CLEANUP_AFTER_MERGE=0
|
|
29
60
|
shift
|
|
30
61
|
;;
|
|
31
62
|
--mode)
|
|
@@ -42,12 +73,16 @@ while [[ $# -gt 0 ]]; do
|
|
|
42
73
|
;;
|
|
43
74
|
*)
|
|
44
75
|
echo "[agent-branch-finish] Unknown argument: $1" >&2
|
|
45
|
-
echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--keep-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
|
|
76
|
+
echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--cleanup|--no-cleanup] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
|
|
46
77
|
exit 1
|
|
47
78
|
;;
|
|
48
79
|
esac
|
|
49
80
|
done
|
|
50
81
|
|
|
82
|
+
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
83
|
+
DELETE_REMOTE_BRANCH=1
|
|
84
|
+
fi
|
|
85
|
+
|
|
51
86
|
case "$MERGE_MODE" in
|
|
52
87
|
auto|direct|pr) ;;
|
|
53
88
|
*)
|
|
@@ -347,43 +382,58 @@ if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
|
|
|
347
382
|
python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
|
|
348
383
|
fi
|
|
349
384
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
385
|
+
base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
|
|
386
|
+
if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
|
|
387
|
+
git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
|
|
391
|
+
if [[ "$source_worktree" == "$repo_root" ]]; then
|
|
392
|
+
if is_clean_worktree "$source_worktree"; then
|
|
393
|
+
git -C "$source_worktree" checkout "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
394
|
+
if [[ "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
|
|
395
|
+
git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
396
|
+
fi
|
|
355
397
|
fi
|
|
398
|
+
elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
|
|
399
|
+
git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
|
|
356
400
|
fi
|
|
357
|
-
elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
|
|
358
|
-
git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
|
|
359
|
-
fi
|
|
360
401
|
|
|
361
|
-
if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
|
|
362
|
-
|
|
363
|
-
fi
|
|
402
|
+
if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
|
|
403
|
+
git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
|
|
404
|
+
fi
|
|
364
405
|
|
|
365
|
-
git -C "$repo_root" branch -d "$SOURCE_BRANCH"
|
|
406
|
+
git -C "$repo_root" branch -d "$SOURCE_BRANCH"
|
|
366
407
|
|
|
367
|
-
if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
368
|
-
|
|
369
|
-
|
|
408
|
+
if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
409
|
+
if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
|
|
410
|
+
git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
|
|
411
|
+
fi
|
|
370
412
|
fi
|
|
371
|
-
fi
|
|
372
413
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
414
|
+
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
415
|
+
prune_args=(--base "$BASE_BRANCH" --delete-branches)
|
|
416
|
+
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
|
|
417
|
+
prune_args+=(--delete-remote-branches)
|
|
418
|
+
fi
|
|
419
|
+
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
420
|
+
echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
|
|
421
|
+
echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
377
424
|
|
|
378
|
-
|
|
379
|
-
if
|
|
380
|
-
echo "[agent-branch-finish]
|
|
381
|
-
echo "[agent-branch-finish]
|
|
425
|
+
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
|
|
426
|
+
if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
|
|
427
|
+
echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
|
|
428
|
+
echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
|
|
429
|
+
fi
|
|
430
|
+
else
|
|
431
|
+
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
432
|
+
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
|
|
433
|
+
echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
|
|
434
|
+
fi
|
|
382
435
|
fi
|
|
383
|
-
fi
|
|
384
436
|
|
|
385
|
-
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and
|
|
386
|
-
|
|
387
|
-
echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
|
|
388
|
-
echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2
|
|
437
|
+
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and kept source branch/worktree."
|
|
438
|
+
echo "[agent-branch-finish] Cleanup later with: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches"
|
|
389
439
|
fi
|
|
@@ -5,6 +5,9 @@ BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
|
|
|
5
5
|
BASE_BRANCH_EXPLICIT=0
|
|
6
6
|
DRY_RUN=0
|
|
7
7
|
FORCE_DIRTY=0
|
|
8
|
+
DELETE_BRANCHES=0
|
|
9
|
+
DELETE_REMOTE_BRANCHES=0
|
|
10
|
+
TARGET_BRANCH=""
|
|
8
11
|
|
|
9
12
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
10
13
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -25,9 +28,21 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
28
|
FORCE_DIRTY=1
|
|
26
29
|
shift
|
|
27
30
|
;;
|
|
31
|
+
--delete-branches)
|
|
32
|
+
DELETE_BRANCHES=1
|
|
33
|
+
shift
|
|
34
|
+
;;
|
|
35
|
+
--delete-remote-branches)
|
|
36
|
+
DELETE_REMOTE_BRANCHES=1
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
--branch)
|
|
40
|
+
TARGET_BRANCH="${2:-}"
|
|
41
|
+
shift 2
|
|
42
|
+
;;
|
|
28
43
|
*)
|
|
29
44
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
30
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty]" >&2
|
|
45
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
|
|
31
46
|
exit 1
|
|
32
47
|
;;
|
|
33
48
|
esac
|
|
@@ -73,6 +88,11 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
|
|
|
73
88
|
exit 1
|
|
74
89
|
fi
|
|
75
90
|
|
|
91
|
+
if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
|
|
92
|
+
echo "[agent-worktree-prune] --branch must reference an agent/* branch: ${TARGET_BRANCH}" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
76
96
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
77
97
|
BASE_BRANCH="$(resolve_base_branch)"
|
|
78
98
|
fi
|
|
@@ -124,6 +144,10 @@ process_entry() {
|
|
|
124
144
|
branch="${branch_ref#refs/heads/}"
|
|
125
145
|
fi
|
|
126
146
|
|
|
147
|
+
if [[ -n "$TARGET_BRANCH" && "$branch" != "$TARGET_BRANCH" ]]; then
|
|
148
|
+
return
|
|
149
|
+
fi
|
|
150
|
+
|
|
127
151
|
if [[ "$wt" == "$current_pwd" ]]; then
|
|
128
152
|
skipped_active=$((skipped_active + 1))
|
|
129
153
|
echo "[agent-worktree-prune] Skipping active cwd worktree: ${wt}"
|
|
@@ -138,7 +162,9 @@ process_entry() {
|
|
|
138
162
|
remove_reason="missing-branch"
|
|
139
163
|
elif [[ "$branch" == agent/* ]]; then
|
|
140
164
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
141
|
-
|
|
165
|
+
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
166
|
+
remove_reason="merged-agent-branch"
|
|
167
|
+
fi
|
|
142
168
|
fi
|
|
143
169
|
elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
|
|
144
170
|
remove_reason="temporary-worktree"
|
|
@@ -163,10 +189,16 @@ process_entry() {
|
|
|
163
189
|
fi
|
|
164
190
|
|
|
165
191
|
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
|
|
166
|
-
if [[ "$branch" == agent/* ]]; then
|
|
192
|
+
if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
167
193
|
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
168
194
|
removed_branches=$((removed_branches + 1))
|
|
169
195
|
echo "[agent-worktree-prune] Deleted merged branch: ${branch}"
|
|
196
|
+
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
197
|
+
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
198
|
+
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
199
|
+
echo "[agent-worktree-prune] Deleted merged remote branch: ${branch}"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
170
202
|
fi
|
|
171
203
|
elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
|
|
172
204
|
run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1 || true
|
|
@@ -199,18 +231,29 @@ done < <(git -C "$repo_root" worktree list --porcelain)
|
|
|
199
231
|
|
|
200
232
|
process_entry "$current_wt" "$current_branch_ref"
|
|
201
233
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
208
|
-
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
209
|
-
removed_branches=$((removed_branches + 1))
|
|
210
|
-
echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
|
|
234
|
+
if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
235
|
+
while IFS= read -r branch; do
|
|
236
|
+
[[ -z "$branch" ]] && continue
|
|
237
|
+
if [[ -n "$TARGET_BRANCH" && "$branch" != "$TARGET_BRANCH" ]]; then
|
|
238
|
+
continue
|
|
211
239
|
fi
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
if branch_has_worktree "$branch"; then
|
|
241
|
+
continue
|
|
242
|
+
fi
|
|
243
|
+
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
244
|
+
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
245
|
+
removed_branches=$((removed_branches + 1))
|
|
246
|
+
echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
|
|
247
|
+
if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
|
|
248
|
+
if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
|
|
249
|
+
run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
|
|
250
|
+
echo "[agent-worktree-prune] Deleted stale merged remote branch: ${branch}"
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/agent)
|
|
256
|
+
fi
|
|
214
257
|
|
|
215
258
|
run_cmd git -C "$repo_root" worktree prune
|
|
216
259
|
|
|
@@ -6,6 +6,26 @@ AGENT_NAME="${MUSAFETY_AGENT_NAME:-agent}"
|
|
|
6
6
|
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
|
|
7
7
|
BASE_BRANCH_EXPLICIT=0
|
|
8
8
|
CODEX_BIN="${MUSAFETY_CODEX_BIN:-codex}"
|
|
9
|
+
AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
|
|
10
|
+
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"
|
|
11
|
+
AUTO_CLEANUP_RAW="${MUSAFETY_CODEX_AUTO_CLEANUP:-false}"
|
|
12
|
+
|
|
13
|
+
normalize_bool() {
|
|
14
|
+
local raw="${1:-}"
|
|
15
|
+
local fallback="${2:-0}"
|
|
16
|
+
local lowered
|
|
17
|
+
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
18
|
+
case "$lowered" in
|
|
19
|
+
1|true|yes|on) printf '1' ;;
|
|
20
|
+
0|false|no|off) printf '0' ;;
|
|
21
|
+
'') printf '%s' "$fallback" ;;
|
|
22
|
+
*) printf '%s' "$fallback" ;;
|
|
23
|
+
esac
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
|
|
27
|
+
AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"
|
|
28
|
+
AUTO_CLEANUP="$(normalize_bool "$AUTO_CLEANUP_RAW" "0")"
|
|
9
29
|
|
|
10
30
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
11
31
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -30,6 +50,30 @@ while [[ $# -gt 0 ]]; do
|
|
|
30
50
|
CODEX_BIN="${2:-$CODEX_BIN}"
|
|
31
51
|
shift 2
|
|
32
52
|
;;
|
|
53
|
+
--auto-finish)
|
|
54
|
+
AUTO_FINISH=1
|
|
55
|
+
shift
|
|
56
|
+
;;
|
|
57
|
+
--no-auto-finish)
|
|
58
|
+
AUTO_FINISH=0
|
|
59
|
+
shift
|
|
60
|
+
;;
|
|
61
|
+
--auto-review-on-conflict)
|
|
62
|
+
AUTO_REVIEW_ON_CONFLICT=1
|
|
63
|
+
shift
|
|
64
|
+
;;
|
|
65
|
+
--no-auto-review-on-conflict)
|
|
66
|
+
AUTO_REVIEW_ON_CONFLICT=0
|
|
67
|
+
shift
|
|
68
|
+
;;
|
|
69
|
+
--cleanup)
|
|
70
|
+
AUTO_CLEANUP=1
|
|
71
|
+
shift
|
|
72
|
+
;;
|
|
73
|
+
--no-cleanup)
|
|
74
|
+
AUTO_CLEANUP=0
|
|
75
|
+
shift
|
|
76
|
+
;;
|
|
33
77
|
--)
|
|
34
78
|
shift
|
|
35
79
|
break
|
|
@@ -95,6 +139,221 @@ if [[ ! -d "$worktree_path" ]]; then
|
|
|
95
139
|
exit 1
|
|
96
140
|
fi
|
|
97
141
|
|
|
142
|
+
has_origin_remote() {
|
|
143
|
+
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
resolve_worktree_base_branch() {
|
|
147
|
+
local wt="$1"
|
|
148
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
|
|
149
|
+
printf '%s' "$BASE_BRANCH"
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
local branch
|
|
154
|
+
branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
155
|
+
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
local stored_base
|
|
160
|
+
stored_base="$(git -C "$repo_root" config --get "branch.${branch}.musafetyBase" || true)"
|
|
161
|
+
if [[ -n "$stored_base" ]]; then
|
|
162
|
+
printf '%s' "$stored_base"
|
|
163
|
+
return 0
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
local configured_base
|
|
167
|
+
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
|
|
168
|
+
if [[ -n "$configured_base" ]]; then
|
|
169
|
+
printf '%s' "$configured_base"
|
|
170
|
+
fi
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sync_worktree_with_base() {
|
|
174
|
+
local wt="$1"
|
|
175
|
+
if ! has_origin_remote; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
local base_branch
|
|
180
|
+
base_branch="$(resolve_worktree_base_branch "$wt")"
|
|
181
|
+
if [[ -z "$base_branch" ]]; then
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
|
|
186
|
+
echo "[codex-agent] Warning: could not fetch origin/${base_branch} before task start." >&2
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
local behind_count
|
|
195
|
+
behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
|
|
196
|
+
behind_count="${behind_count:-0}"
|
|
197
|
+
if [[ "$behind_count" -le 0 ]]; then
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
local branch
|
|
202
|
+
branch="$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
203
|
+
echo "[codex-agent] Task sync: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Rebasing before launch..."
|
|
204
|
+
if ! git -C "$wt" rebase "origin/${base_branch}"; then
|
|
205
|
+
echo "[codex-agent] Task sync failed. Resolve and continue in sandbox:" >&2
|
|
206
|
+
echo " git -C \"$wt\" rebase --continue" >&2
|
|
207
|
+
echo " # or abort" >&2
|
|
208
|
+
echo " git -C \"$wt\" rebase --abort" >&2
|
|
209
|
+
return 1
|
|
210
|
+
fi
|
|
211
|
+
echo "[codex-agent] Task sync complete."
|
|
212
|
+
return 0
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
worktree_has_changes() {
|
|
216
|
+
local wt="$1"
|
|
217
|
+
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
if ! git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
if [[ -n "$(git -C "$wt" ls-files --others --exclude-standard)" ]]; then
|
|
224
|
+
return 0
|
|
225
|
+
fi
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
claim_changed_files() {
|
|
230
|
+
local wt="$1"
|
|
231
|
+
local branch="$2"
|
|
232
|
+
local lock_script="${repo_root}/scripts/agent-file-locks.py"
|
|
233
|
+
|
|
234
|
+
if [[ ! -x "$lock_script" ]]; then
|
|
235
|
+
return 0
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
local changed_raw deleted_raw
|
|
239
|
+
changed_raw="$({
|
|
240
|
+
git -C "$wt" diff --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
241
|
+
git -C "$wt" diff --cached --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
242
|
+
git -C "$wt" ls-files --others --exclude-standard;
|
|
243
|
+
} | sed '/^$/d' | sort -u)"
|
|
244
|
+
|
|
245
|
+
if [[ -n "$changed_raw" ]]; then
|
|
246
|
+
mapfile -t changed_files < <(printf '%s\n' "$changed_raw")
|
|
247
|
+
python3 "$lock_script" claim --branch "$branch" "${changed_files[@]}" >/dev/null 2>&1 || true
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
deleted_raw="$({
|
|
251
|
+
git -C "$wt" diff --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
252
|
+
git -C "$wt" diff --cached --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
|
|
253
|
+
} | sed '/^$/d' | sort -u)"
|
|
254
|
+
|
|
255
|
+
if [[ -n "$deleted_raw" ]]; then
|
|
256
|
+
mapfile -t deleted_files < <(printf '%s\n' "$deleted_raw")
|
|
257
|
+
python3 "$lock_script" allow-delete --branch "$branch" "${deleted_files[@]}" >/dev/null 2>&1 || true
|
|
258
|
+
fi
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
auto_commit_worktree_changes() {
|
|
262
|
+
local wt="$1"
|
|
263
|
+
local branch="$2"
|
|
264
|
+
|
|
265
|
+
if ! worktree_has_changes "$wt"; then
|
|
266
|
+
return 0
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
claim_changed_files "$wt" "$branch"
|
|
270
|
+
git -C "$wt" add -A
|
|
271
|
+
|
|
272
|
+
if git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
|
|
273
|
+
return 0
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
local default_message="Auto-finish: ${TASK_NAME}"
|
|
277
|
+
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
|
|
278
|
+
|
|
279
|
+
if ! git -C "$wt" commit -m "$commit_message" >/dev/null 2>&1; then
|
|
280
|
+
echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
|
|
281
|
+
return 1
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
|
|
285
|
+
return 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
looks_like_conflict_failure() {
|
|
289
|
+
local output="$1"
|
|
290
|
+
if grep -qiE 'preflight conflict detected|merge conflict detected|auto-sync failed while rebasing|rebase --continue|rebase --abort' <<< "$output"; then
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
return 1
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
run_finish_flow() {
|
|
297
|
+
local wt="$1"
|
|
298
|
+
local branch="$2"
|
|
299
|
+
local finish_output=""
|
|
300
|
+
local -a finish_args
|
|
301
|
+
|
|
302
|
+
finish_args=(--branch "$branch")
|
|
303
|
+
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
304
|
+
finish_args+=(--base "$BASE_BRANCH")
|
|
305
|
+
fi
|
|
306
|
+
if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
307
|
+
finish_args+=(--cleanup)
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
if has_origin_remote; then
|
|
311
|
+
if command -v gh >/dev/null 2>&1 || command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1; then
|
|
312
|
+
finish_args+=(--via-pr)
|
|
313
|
+
fi
|
|
314
|
+
else
|
|
315
|
+
echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
|
|
316
|
+
return 2
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
|
|
320
|
+
printf '%s\n' "$finish_output"
|
|
321
|
+
return 0
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
printf '%s\n' "$finish_output" >&2
|
|
325
|
+
|
|
326
|
+
if [[ "$AUTO_REVIEW_ON_CONFLICT" -eq 1 ]] && looks_like_conflict_failure "$finish_output"; then
|
|
327
|
+
echo "[codex-agent] Auto-finish hit conflicts. Launching Codex conflict-review pass in sandbox..." >&2
|
|
328
|
+
local review_prompt
|
|
329
|
+
review_prompt="Resolve git conflicts for branch ${branch} against ${BASE_BRANCH:-base branch}, then commit the resolution in this sandbox worktree and exit."
|
|
330
|
+
|
|
331
|
+
(
|
|
332
|
+
cd "$wt"
|
|
333
|
+
set +e
|
|
334
|
+
"$CODEX_BIN" "$review_prompt"
|
|
335
|
+
review_exit="$?"
|
|
336
|
+
set -e
|
|
337
|
+
if [[ "$review_exit" -ne 0 ]]; then
|
|
338
|
+
echo "[codex-agent] Conflict-review Codex pass exited with status ${review_exit}." >&2
|
|
339
|
+
fi
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
|
|
343
|
+
printf '%s\n' "$finish_output"
|
|
344
|
+
return 0
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
printf '%s\n' "$finish_output" >&2
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
return 1
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if ! sync_worktree_with_base "$worktree_path"; then
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
356
|
+
|
|
98
357
|
echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
|
|
99
358
|
cd "$worktree_path"
|
|
100
359
|
set +e
|
|
@@ -103,6 +362,38 @@ codex_exit="$?"
|
|
|
103
362
|
set -e
|
|
104
363
|
|
|
105
364
|
cd "$repo_root"
|
|
365
|
+
final_exit="$codex_exit"
|
|
366
|
+
auto_finish_completed=0
|
|
367
|
+
|
|
368
|
+
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
369
|
+
|
|
370
|
+
if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
371
|
+
if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
372
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge -> cleanup."
|
|
373
|
+
else
|
|
374
|
+
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
|
|
375
|
+
fi
|
|
376
|
+
if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
|
|
377
|
+
if run_finish_flow "$worktree_path" "$worktree_branch"; then
|
|
378
|
+
auto_finish_completed=1
|
|
379
|
+
echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
|
|
380
|
+
else
|
|
381
|
+
finish_status="$?"
|
|
382
|
+
if [[ "$finish_status" -eq 2 ]]; then
|
|
383
|
+
echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
|
|
384
|
+
else
|
|
385
|
+
echo "[codex-agent] Auto-finish did not complete; keeping sandbox for manual review: $worktree_path" >&2
|
|
386
|
+
if [[ "$final_exit" -eq 0 ]]; then
|
|
387
|
+
final_exit=1
|
|
388
|
+
fi
|
|
389
|
+
fi
|
|
390
|
+
fi
|
|
391
|
+
else
|
|
392
|
+
if [[ "$final_exit" -eq 0 ]]; then
|
|
393
|
+
final_exit=1
|
|
394
|
+
fi
|
|
395
|
+
fi
|
|
396
|
+
fi
|
|
106
397
|
|
|
107
398
|
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
108
399
|
echo "[codex-agent] Session ended (exit=${codex_exit}). Running worktree cleanup..."
|
|
@@ -110,6 +401,9 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
|
|
|
110
401
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
|
|
111
402
|
prune_args+=(--base "$BASE_BRANCH")
|
|
112
403
|
fi
|
|
404
|
+
if [[ "$AUTO_CLEANUP" -eq 1 ]]; then
|
|
405
|
+
prune_args+=(--delete-branches --delete-remote-branches)
|
|
406
|
+
fi
|
|
113
407
|
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
|
|
114
408
|
echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
|
|
115
409
|
fi
|
|
@@ -121,8 +415,13 @@ else
|
|
|
121
415
|
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
122
416
|
echo "[codex-agent] Sandbox worktree kept: $worktree_path"
|
|
123
417
|
if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
|
|
124
|
-
|
|
418
|
+
if [[ "$auto_finish_completed" -eq 1 ]]; then
|
|
419
|
+
echo "[codex-agent] Branch kept intentionally. Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
420
|
+
else
|
|
421
|
+
echo "[codex-agent] If finished, merge with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --via-pr"
|
|
422
|
+
echo "[codex-agent] Cleanup on demand: gx cleanup --branch \"${worktree_branch}\""
|
|
423
|
+
fi
|
|
125
424
|
fi
|
|
126
425
|
fi
|
|
127
426
|
|
|
128
|
-
exit "$
|
|
427
|
+
exit "$final_exit"
|