@imdeadpool/guardex 5.0.4 → 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 +17 -2
- package/bin/multiagent-safety.js +172 -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,20 @@ 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
|
+
|
|
252
|
+
### v5.0.5
|
|
253
|
+
|
|
254
|
+
- Bumped package version from `5.0.4` to `5.0.5` so npm publish can proceed with the next patch release.
|
|
255
|
+
|
|
241
256
|
### v5.0.4
|
|
242
257
|
|
|
243
258
|
- Bumped package version from `5.0.3` to `5.0.4` to stay one patch ahead of the current npm published version.
|
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 = [
|
|
@@ -147,6 +148,10 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
147
148
|
['help', 'Show this help output'],
|
|
148
149
|
['version', 'Print GuardeX version'],
|
|
149
150
|
];
|
|
151
|
+
const AGENT_BOT_DESCRIPTIONS = [
|
|
152
|
+
['review', 'Monitor open PRs targeting current branch and dispatch codex-agent review flow'],
|
|
153
|
+
['start', 'bash scripts/review-bot-watch.sh --interval 30'],
|
|
154
|
+
];
|
|
150
155
|
|
|
151
156
|
const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
|
|
152
157
|
|
|
@@ -261,9 +266,20 @@ function commandCatalogLines(indent = ' ') {
|
|
|
261
266
|
);
|
|
262
267
|
}
|
|
263
268
|
|
|
269
|
+
function agentBotCatalogLines(indent = ' ') {
|
|
270
|
+
const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
|
|
271
|
+
(max, [command]) => Math.max(max, command.length),
|
|
272
|
+
0,
|
|
273
|
+
);
|
|
274
|
+
return AGENT_BOT_DESCRIPTIONS.map(
|
|
275
|
+
([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
function printToolLogsSummary() {
|
|
265
280
|
const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
|
|
266
281
|
const commandDetails = commandCatalogLines(' ');
|
|
282
|
+
const agentBotDetails = agentBotCatalogLines(' ');
|
|
267
283
|
|
|
268
284
|
if (!supportsAnsiColors()) {
|
|
269
285
|
console.log(`${TOOL_NAME}-tools logs:`);
|
|
@@ -273,12 +289,17 @@ function printToolLogsSummary() {
|
|
|
273
289
|
for (const line of commandDetails) {
|
|
274
290
|
console.log(line);
|
|
275
291
|
}
|
|
292
|
+
console.log(' AGENT BOT');
|
|
293
|
+
for (const line of agentBotDetails) {
|
|
294
|
+
console.log(line);
|
|
295
|
+
}
|
|
276
296
|
return;
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
const title = colorize(`${TOOL_NAME}-tools logs`, '1;36');
|
|
280
300
|
const usageHeader = colorize('USAGE', '1');
|
|
281
301
|
const commandsHeader = colorize('COMMANDS', '1');
|
|
302
|
+
const agentBotHeader = colorize('AGENT BOT', '1');
|
|
282
303
|
const pipe = colorize('│', '90');
|
|
283
304
|
const tee = colorize('├', '90');
|
|
284
305
|
const corner = colorize('└', '90');
|
|
@@ -294,6 +315,14 @@ function printToolLogsSummary() {
|
|
|
294
315
|
}
|
|
295
316
|
console.log(` ${pipe}${line.slice(2)}`);
|
|
296
317
|
}
|
|
318
|
+
console.log(` ${tee}─ ${agentBotHeader}`);
|
|
319
|
+
for (const line of agentBotDetails) {
|
|
320
|
+
if (!line) {
|
|
321
|
+
console.log(` ${pipe}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
console.log(` ${pipe}${line.slice(2)}`);
|
|
325
|
+
}
|
|
297
326
|
console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
|
|
298
327
|
}
|
|
299
328
|
|
|
@@ -311,6 +340,9 @@ USAGE
|
|
|
311
340
|
COMMANDS
|
|
312
341
|
${commandCatalogLines().join('\n')}
|
|
313
342
|
|
|
343
|
+
AGENT BOT
|
|
344
|
+
${agentBotCatalogLines().join('\n')}
|
|
345
|
+
|
|
314
346
|
NOTES
|
|
315
347
|
- Running ${TOOL_NAME} with no command defaults to: ${SHORT_TOOL_NAME} status
|
|
316
348
|
- Short alias: ${SHORT_TOOL_NAME}
|
|
@@ -577,6 +609,10 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
577
609
|
function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
578
610
|
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
579
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
|
+
);
|
|
580
616
|
|
|
581
617
|
if (!fs.existsSync(agentsPath)) {
|
|
582
618
|
if (!dryRun) {
|
|
@@ -586,8 +622,19 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
|
586
622
|
}
|
|
587
623
|
|
|
588
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
|
+
|
|
589
636
|
if (existing.includes(AGENTS_MARKER_START)) {
|
|
590
|
-
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
637
|
+
return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
|
|
591
638
|
}
|
|
592
639
|
|
|
593
640
|
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
@@ -1038,6 +1085,19 @@ function isCommandAvailable(commandName) {
|
|
|
1038
1085
|
return run('which', [commandName]).status === 0;
|
|
1039
1086
|
}
|
|
1040
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
|
+
|
|
1041
1101
|
function finishDoctorSandboxBranch(blocked, metadata) {
|
|
1042
1102
|
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1043
1103
|
if (!fs.existsSync(finishScript)) {
|
|
@@ -1091,6 +1151,17 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1091
1151
|
};
|
|
1092
1152
|
}
|
|
1093
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
|
+
|
|
1094
1165
|
return {
|
|
1095
1166
|
status: 'completed',
|
|
1096
1167
|
note: 'doctor sandbox finish flow completed',
|
|
@@ -1235,6 +1306,15 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1235
1306
|
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
1236
1307
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1237
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);
|
|
1238
1318
|
} else if (finishResult.status === 'failed') {
|
|
1239
1319
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
1240
1320
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
@@ -1499,6 +1579,88 @@ function uniquePreserveOrder(items) {
|
|
|
1499
1579
|
return result;
|
|
1500
1580
|
}
|
|
1501
1581
|
|
|
1582
|
+
function readConfiguredProtectedBranches(repoRoot) {
|
|
1583
|
+
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
1584
|
+
if (result.status !== 0) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
const parsed = uniquePreserveOrder(parseBranchList(result.stdout.trim()));
|
|
1588
|
+
if (parsed.length === 0) {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
return parsed;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function listLocalUserBranches(repoRoot) {
|
|
1595
|
+
const result = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], { allowFailure: true });
|
|
1596
|
+
const branchNames = result.status === 0
|
|
1597
|
+
? uniquePreserveOrder(
|
|
1598
|
+
String(result.stdout || '')
|
|
1599
|
+
.split('\n')
|
|
1600
|
+
.map((item) => item.trim())
|
|
1601
|
+
.filter(Boolean),
|
|
1602
|
+
)
|
|
1603
|
+
: [];
|
|
1604
|
+
|
|
1605
|
+
const additionalUserBranches = branchNames.filter(
|
|
1606
|
+
(branchName) =>
|
|
1607
|
+
!branchName.startsWith('agent/') &&
|
|
1608
|
+
!DEFAULT_PROTECTED_BRANCHES.includes(branchName),
|
|
1609
|
+
);
|
|
1610
|
+
if (additionalUserBranches.length > 0) {
|
|
1611
|
+
return additionalUserBranches;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const current = gitRun(repoRoot, ['branch', '--show-current'], { allowFailure: true });
|
|
1615
|
+
if (current.status !== 0) {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const branchName = String(current.stdout || '').trim();
|
|
1620
|
+
if (
|
|
1621
|
+
!branchName ||
|
|
1622
|
+
branchName.startsWith('agent/') ||
|
|
1623
|
+
DEFAULT_PROTECTED_BRANCHES.includes(branchName)
|
|
1624
|
+
) {
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return [branchName];
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
1632
|
+
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
1633
|
+
if (localUserBranches.length === 0) {
|
|
1634
|
+
return {
|
|
1635
|
+
status: 'unchanged',
|
|
1636
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1637
|
+
note: 'no additional local user branches detected',
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const configured = readConfiguredProtectedBranches(repoRoot);
|
|
1642
|
+
const currentBranches = configured || [...DEFAULT_PROTECTED_BRANCHES];
|
|
1643
|
+
const missingBranches = localUserBranches.filter((branchName) => !currentBranches.includes(branchName));
|
|
1644
|
+
if (missingBranches.length === 0) {
|
|
1645
|
+
return {
|
|
1646
|
+
status: 'unchanged',
|
|
1647
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1648
|
+
note: 'local user branches already protected',
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const nextBranches = uniquePreserveOrder([...currentBranches, ...missingBranches]);
|
|
1653
|
+
if (!dryRun) {
|
|
1654
|
+
writeProtectedBranches(repoRoot, nextBranches);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
return {
|
|
1658
|
+
status: dryRun ? 'would-update' : 'updated',
|
|
1659
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1660
|
+
note: `added local user branch(es): ${missingBranches.join(', ')}`,
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1502
1664
|
function readProtectedBranches(repoRoot) {
|
|
1503
1665
|
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
1504
1666
|
if (result.status !== 0) {
|
|
@@ -1705,6 +1867,7 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1705
1867
|
dryRun: false,
|
|
1706
1868
|
forceDirty: false,
|
|
1707
1869
|
keepRemote: false,
|
|
1870
|
+
keepCleanWorktrees: false,
|
|
1708
1871
|
};
|
|
1709
1872
|
|
|
1710
1873
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -1748,6 +1911,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
1748
1911
|
options.keepRemote = true;
|
|
1749
1912
|
continue;
|
|
1750
1913
|
}
|
|
1914
|
+
if (arg === '--keep-clean-worktrees') {
|
|
1915
|
+
options.keepCleanWorktrees = true;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1751
1918
|
throw new Error(`Unknown option: ${arg}`);
|
|
1752
1919
|
}
|
|
1753
1920
|
|
|
@@ -2768,6 +2935,7 @@ function setup(rawArgs) {
|
|
|
2768
2935
|
|
|
2769
2936
|
assertProtectedMainWriteAllowed(options, 'setup');
|
|
2770
2937
|
const installPayload = runInstallInternal(options);
|
|
2938
|
+
installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)));
|
|
2771
2939
|
printOperations('Setup/install', installPayload, options.dryRun);
|
|
2772
2940
|
|
|
2773
2941
|
const fixPayload = runFixInternal({
|
|
@@ -2882,6 +3050,9 @@ function cleanup(rawArgs) {
|
|
|
2882
3050
|
if (options.dryRun) {
|
|
2883
3051
|
args.push('--dry-run');
|
|
2884
3052
|
}
|
|
3053
|
+
if (!options.keepCleanWorktrees) {
|
|
3054
|
+
args.push('--only-dirty-worktrees');
|
|
3055
|
+
}
|
|
2885
3056
|
args.push('--delete-branches');
|
|
2886
3057
|
if (!options.keepRemote) {
|
|
2887
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
|