@imdeadpool/guardex 5.0.3 → 5.0.5
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
CHANGED
|
@@ -107,6 +107,9 @@ gx protect remove release
|
|
|
107
107
|
gx sync --check
|
|
108
108
|
gx sync
|
|
109
109
|
|
|
110
|
+
# continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
|
|
111
|
+
bash scripts/review-bot-watch.sh --interval 30
|
|
112
|
+
|
|
110
113
|
# cleanup merged agent branches/worktrees
|
|
111
114
|
gx cleanup
|
|
112
115
|
|
|
@@ -115,6 +118,23 @@ gx scan
|
|
|
115
118
|
gx report scorecard --repo github.com/recodeecom/multiagent-safety
|
|
116
119
|
```
|
|
117
120
|
|
|
121
|
+
### Continuous Codex PR monitor (local codex-auth session)
|
|
122
|
+
|
|
123
|
+
Run this in your local shell to keep watching PRs targeting the current branch (or `--base <branch>`):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
bash scripts/review-bot-watch.sh --interval 30
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Useful flags:
|
|
130
|
+
|
|
131
|
+
- `--base main` watch a specific base branch
|
|
132
|
+
- `--only-pr 123` process only one PR
|
|
133
|
+
- `--once` run one polling cycle and exit
|
|
134
|
+
- `--retry-failed` retry failed PRs without waiting for a new head SHA
|
|
135
|
+
|
|
136
|
+
Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flags for compatibility with both older and newer `scripts/codex-agent.sh` argument parsing.
|
|
137
|
+
|
|
118
138
|
## Important behavior defaults
|
|
119
139
|
|
|
120
140
|
- No command defaults to `gx status`.
|
|
@@ -123,6 +143,8 @@ gx report scorecard --repo github.com/recodeecom/multiagent-safety
|
|
|
123
143
|
- `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
|
|
124
144
|
- Interactive self-update prompt defaults to **No** (`[y/N]`).
|
|
125
145
|
- In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
|
|
146
|
+
- In VS Code Source Control, manual (non-Codex) commits/pushes to protected branches are allowed by default.
|
|
147
|
+
- Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
|
|
126
148
|
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
|
|
127
149
|
- `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
|
|
128
150
|
|
|
@@ -185,11 +207,13 @@ codex-auth current
|
|
|
185
207
|
scripts/agent-branch-start.sh
|
|
186
208
|
scripts/agent-branch-finish.sh
|
|
187
209
|
scripts/codex-agent.sh
|
|
210
|
+
scripts/review-bot-watch.sh
|
|
188
211
|
scripts/agent-worktree-prune.sh
|
|
189
212
|
scripts/agent-file-locks.py
|
|
190
213
|
scripts/install-agent-git-hooks.sh
|
|
191
214
|
scripts/openspec/init-plan-workspace.sh
|
|
192
215
|
.githooks/pre-commit
|
|
216
|
+
.githooks/pre-push
|
|
193
217
|
.codex/skills/guardex/SKILL.md
|
|
194
218
|
.claude/commands/guardex.md
|
|
195
219
|
.omx/state/agent-file-locks.json
|
|
@@ -214,6 +238,14 @@ npm pack --dry-run
|
|
|
214
238
|
|
|
215
239
|
## Release notes
|
|
216
240
|
|
|
241
|
+
### v5.0.5
|
|
242
|
+
|
|
243
|
+
- Bumped package version from `5.0.4` to `5.0.5` so npm publish can proceed with the next patch release.
|
|
244
|
+
|
|
245
|
+
### v5.0.4
|
|
246
|
+
|
|
247
|
+
- Bumped package version from `5.0.3` to `5.0.4` to stay one patch ahead of the current npm published version.
|
|
248
|
+
|
|
217
249
|
### v5.0.3
|
|
218
250
|
|
|
219
251
|
- Bumped package version from `5.0.2` to `5.0.3` for the next npm publish.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -19,6 +19,7 @@ const GH_BIN = process.env.MUSAFETY_GH_BIN || 'gh';
|
|
|
19
19
|
const REQUIRED_SYSTEM_TOOLS = [
|
|
20
20
|
{
|
|
21
21
|
name: 'gh',
|
|
22
|
+
displayName: 'GitHub (gh)',
|
|
22
23
|
command: GH_BIN,
|
|
23
24
|
installHint: 'https://cli.github.com/',
|
|
24
25
|
},
|
|
@@ -41,11 +42,13 @@ const TEMPLATE_FILES = [
|
|
|
41
42
|
'scripts/agent-branch-start.sh',
|
|
42
43
|
'scripts/agent-branch-finish.sh',
|
|
43
44
|
'scripts/codex-agent.sh',
|
|
45
|
+
'scripts/review-bot-watch.sh',
|
|
44
46
|
'scripts/agent-worktree-prune.sh',
|
|
45
47
|
'scripts/agent-file-locks.py',
|
|
46
48
|
'scripts/install-agent-git-hooks.sh',
|
|
47
49
|
'scripts/openspec/init-plan-workspace.sh',
|
|
48
50
|
'githooks/pre-commit',
|
|
51
|
+
'githooks/pre-push',
|
|
49
52
|
'codex/skills/guardex/SKILL.md',
|
|
50
53
|
'claude/commands/guardex.md',
|
|
51
54
|
];
|
|
@@ -54,16 +57,19 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
|
54
57
|
'scripts/agent-branch-start.sh',
|
|
55
58
|
'scripts/agent-branch-finish.sh',
|
|
56
59
|
'scripts/codex-agent.sh',
|
|
60
|
+
'scripts/review-bot-watch.sh',
|
|
57
61
|
'scripts/agent-worktree-prune.sh',
|
|
58
62
|
'scripts/agent-file-locks.py',
|
|
59
63
|
'scripts/install-agent-git-hooks.sh',
|
|
60
64
|
'scripts/openspec/init-plan-workspace.sh',
|
|
61
65
|
'.githooks/pre-commit',
|
|
66
|
+
'.githooks/pre-push',
|
|
62
67
|
]);
|
|
63
68
|
|
|
64
69
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
65
70
|
'AGENTS.md',
|
|
66
71
|
'.githooks/pre-commit',
|
|
72
|
+
'.githooks/pre-push',
|
|
67
73
|
'scripts/agent-branch-start.sh',
|
|
68
74
|
'scripts/agent-branch-finish.sh',
|
|
69
75
|
'scripts/agent-worktree-prune.sh',
|
|
@@ -79,11 +85,13 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
79
85
|
'scripts/agent-branch-start.sh',
|
|
80
86
|
'scripts/agent-branch-finish.sh',
|
|
81
87
|
'scripts/codex-agent.sh',
|
|
88
|
+
'scripts/review-bot-watch.sh',
|
|
82
89
|
'scripts/agent-worktree-prune.sh',
|
|
83
90
|
'scripts/agent-file-locks.py',
|
|
84
91
|
'scripts/install-agent-git-hooks.sh',
|
|
85
92
|
'scripts/openspec/init-plan-workspace.sh',
|
|
86
93
|
'.githooks/pre-commit',
|
|
94
|
+
'.githooks/pre-push',
|
|
87
95
|
'oh-my-codex/',
|
|
88
96
|
'.codex/skills/guardex/SKILL.md',
|
|
89
97
|
'.claude/commands/guardex.md',
|
|
@@ -139,6 +147,10 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
139
147
|
['help', 'Show this help output'],
|
|
140
148
|
['version', 'Print GuardeX version'],
|
|
141
149
|
];
|
|
150
|
+
const AGENT_BOT_DESCRIPTIONS = [
|
|
151
|
+
['review', 'Monitor open PRs targeting current branch and dispatch codex-agent review flow'],
|
|
152
|
+
['start', 'bash scripts/review-bot-watch.sh --interval 30'],
|
|
153
|
+
];
|
|
142
154
|
|
|
143
155
|
const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
|
|
144
156
|
|
|
@@ -253,9 +265,20 @@ function commandCatalogLines(indent = ' ') {
|
|
|
253
265
|
);
|
|
254
266
|
}
|
|
255
267
|
|
|
268
|
+
function agentBotCatalogLines(indent = ' ') {
|
|
269
|
+
const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
|
|
270
|
+
(max, [command]) => Math.max(max, command.length),
|
|
271
|
+
0,
|
|
272
|
+
);
|
|
273
|
+
return AGENT_BOT_DESCRIPTIONS.map(
|
|
274
|
+
([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
256
278
|
function printToolLogsSummary() {
|
|
257
279
|
const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
|
|
258
280
|
const commandDetails = commandCatalogLines(' ');
|
|
281
|
+
const agentBotDetails = agentBotCatalogLines(' ');
|
|
259
282
|
|
|
260
283
|
if (!supportsAnsiColors()) {
|
|
261
284
|
console.log(`${TOOL_NAME}-tools logs:`);
|
|
@@ -265,12 +288,17 @@ function printToolLogsSummary() {
|
|
|
265
288
|
for (const line of commandDetails) {
|
|
266
289
|
console.log(line);
|
|
267
290
|
}
|
|
291
|
+
console.log(' AGENT BOT');
|
|
292
|
+
for (const line of agentBotDetails) {
|
|
293
|
+
console.log(line);
|
|
294
|
+
}
|
|
268
295
|
return;
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
const title = colorize(`${TOOL_NAME}-tools logs`, '1;36');
|
|
272
299
|
const usageHeader = colorize('USAGE', '1');
|
|
273
300
|
const commandsHeader = colorize('COMMANDS', '1');
|
|
301
|
+
const agentBotHeader = colorize('AGENT BOT', '1');
|
|
274
302
|
const pipe = colorize('│', '90');
|
|
275
303
|
const tee = colorize('├', '90');
|
|
276
304
|
const corner = colorize('└', '90');
|
|
@@ -286,6 +314,14 @@ function printToolLogsSummary() {
|
|
|
286
314
|
}
|
|
287
315
|
console.log(` ${pipe}${line.slice(2)}`);
|
|
288
316
|
}
|
|
317
|
+
console.log(` ${tee}─ ${agentBotHeader}`);
|
|
318
|
+
for (const line of agentBotDetails) {
|
|
319
|
+
if (!line) {
|
|
320
|
+
console.log(` ${pipe}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
console.log(` ${pipe}${line.slice(2)}`);
|
|
324
|
+
}
|
|
289
325
|
console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
|
|
290
326
|
}
|
|
291
327
|
|
|
@@ -303,6 +339,9 @@ USAGE
|
|
|
303
339
|
COMMANDS
|
|
304
340
|
${commandCatalogLines().join('\n')}
|
|
305
341
|
|
|
342
|
+
AGENT BOT
|
|
343
|
+
${agentBotCatalogLines().join('\n')}
|
|
344
|
+
|
|
306
345
|
NOTES
|
|
307
346
|
- Running ${TOOL_NAME} with no command defaults to: ${SHORT_TOOL_NAME} status
|
|
308
347
|
- Short alias: ${SHORT_TOOL_NAME}
|
|
@@ -527,6 +566,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
|
|
|
527
566
|
|
|
528
567
|
const wantedScripts = {
|
|
529
568
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
569
|
+
'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
|
|
530
570
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
531
571
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
532
572
|
'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
|
|
@@ -706,6 +746,7 @@ function hasGuardexBootstrapFiles(repoRoot) {
|
|
|
706
746
|
'AGENTS.md',
|
|
707
747
|
'scripts/agent-branch-start.sh',
|
|
708
748
|
'.githooks/pre-commit',
|
|
749
|
+
'.githooks/pre-push',
|
|
709
750
|
LOCK_FILE_RELATIVE,
|
|
710
751
|
];
|
|
711
752
|
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
@@ -1489,6 +1530,88 @@ function uniquePreserveOrder(items) {
|
|
|
1489
1530
|
return result;
|
|
1490
1531
|
}
|
|
1491
1532
|
|
|
1533
|
+
function readConfiguredProtectedBranches(repoRoot) {
|
|
1534
|
+
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
1535
|
+
if (result.status !== 0) {
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
const parsed = uniquePreserveOrder(parseBranchList(result.stdout.trim()));
|
|
1539
|
+
if (parsed.length === 0) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
return parsed;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function listLocalUserBranches(repoRoot) {
|
|
1546
|
+
const result = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], { allowFailure: true });
|
|
1547
|
+
const branchNames = result.status === 0
|
|
1548
|
+
? uniquePreserveOrder(
|
|
1549
|
+
String(result.stdout || '')
|
|
1550
|
+
.split('\n')
|
|
1551
|
+
.map((item) => item.trim())
|
|
1552
|
+
.filter(Boolean),
|
|
1553
|
+
)
|
|
1554
|
+
: [];
|
|
1555
|
+
|
|
1556
|
+
const additionalUserBranches = branchNames.filter(
|
|
1557
|
+
(branchName) =>
|
|
1558
|
+
!branchName.startsWith('agent/') &&
|
|
1559
|
+
!DEFAULT_PROTECTED_BRANCHES.includes(branchName),
|
|
1560
|
+
);
|
|
1561
|
+
if (additionalUserBranches.length > 0) {
|
|
1562
|
+
return additionalUserBranches;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const current = gitRun(repoRoot, ['branch', '--show-current'], { allowFailure: true });
|
|
1566
|
+
if (current.status !== 0) {
|
|
1567
|
+
return [];
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const branchName = String(current.stdout || '').trim();
|
|
1571
|
+
if (
|
|
1572
|
+
!branchName ||
|
|
1573
|
+
branchName.startsWith('agent/') ||
|
|
1574
|
+
DEFAULT_PROTECTED_BRANCHES.includes(branchName)
|
|
1575
|
+
) {
|
|
1576
|
+
return [];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return [branchName];
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
1583
|
+
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
1584
|
+
if (localUserBranches.length === 0) {
|
|
1585
|
+
return {
|
|
1586
|
+
status: 'unchanged',
|
|
1587
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1588
|
+
note: 'no additional local user branches detected',
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const configured = readConfiguredProtectedBranches(repoRoot);
|
|
1593
|
+
const currentBranches = configured || [...DEFAULT_PROTECTED_BRANCHES];
|
|
1594
|
+
const missingBranches = localUserBranches.filter((branchName) => !currentBranches.includes(branchName));
|
|
1595
|
+
if (missingBranches.length === 0) {
|
|
1596
|
+
return {
|
|
1597
|
+
status: 'unchanged',
|
|
1598
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1599
|
+
note: 'local user branches already protected',
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const nextBranches = uniquePreserveOrder([...currentBranches, ...missingBranches]);
|
|
1604
|
+
if (!dryRun) {
|
|
1605
|
+
writeProtectedBranches(repoRoot, nextBranches);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
return {
|
|
1609
|
+
status: dryRun ? 'would-update' : 'updated',
|
|
1610
|
+
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
1611
|
+
note: `added local user branch(es): ${missingBranches.join(', ')}`,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1492
1615
|
function readProtectedBranches(repoRoot) {
|
|
1493
1616
|
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
1494
1617
|
if (result.status !== 0) {
|
|
@@ -2052,6 +2175,7 @@ function detectRequiredSystemTools() {
|
|
|
2052
2175
|
const reason = rawReason.split('\n')[0] || '';
|
|
2053
2176
|
services.push({
|
|
2054
2177
|
name: tool.name,
|
|
2178
|
+
displayName: tool.displayName || tool.name,
|
|
2055
2179
|
command: tool.command,
|
|
2056
2180
|
installHint: tool.installHint,
|
|
2057
2181
|
status: active ? 'active' : 'inactive',
|
|
@@ -2404,6 +2528,7 @@ function status(rawArgs) {
|
|
|
2404
2528
|
...npmServices,
|
|
2405
2529
|
...requiredSystemTools.map((tool) => ({
|
|
2406
2530
|
name: tool.name,
|
|
2531
|
+
displayName: tool.displayName || tool.name,
|
|
2407
2532
|
status: tool.status,
|
|
2408
2533
|
})),
|
|
2409
2534
|
];
|
|
@@ -2452,11 +2577,14 @@ function status(rawArgs) {
|
|
|
2452
2577
|
|
|
2453
2578
|
console.log(`[${TOOL_NAME}] Global services:`);
|
|
2454
2579
|
for (const service of services) {
|
|
2455
|
-
|
|
2580
|
+
const serviceLabel = service.displayName || service.name;
|
|
2581
|
+
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
|
|
2456
2582
|
}
|
|
2457
2583
|
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
2458
2584
|
if (missingSystemTools.length > 0) {
|
|
2459
|
-
const tools = missingSystemTools
|
|
2585
|
+
const tools = missingSystemTools
|
|
2586
|
+
.map((tool) => tool.displayName || tool.name)
|
|
2587
|
+
.join(', ');
|
|
2460
2588
|
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${tools}`);
|
|
2461
2589
|
for (const tool of missingSystemTools) {
|
|
2462
2590
|
const reasonText = tool.reason ? ` (${tool.reason})` : '';
|
|
@@ -2474,6 +2602,16 @@ function status(rawArgs) {
|
|
|
2474
2602
|
|
|
2475
2603
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
2476
2604
|
console.log(`[${TOOL_NAME}] Repo safety service: ${statusDot('active')} active.`);
|
|
2605
|
+
} else if (scanResult.errors === 0) {
|
|
2606
|
+
console.log(
|
|
2607
|
+
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.warnings} warning(s)).`,
|
|
2608
|
+
);
|
|
2609
|
+
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' to review warning details.`);
|
|
2610
|
+
} else if (scanResult.warnings === 0) {
|
|
2611
|
+
console.log(
|
|
2612
|
+
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s)).`,
|
|
2613
|
+
);
|
|
2614
|
+
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' for detailed findings.`);
|
|
2477
2615
|
} else {
|
|
2478
2616
|
console.log(
|
|
2479
2617
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
@@ -2743,6 +2881,7 @@ function setup(rawArgs) {
|
|
|
2743
2881
|
|
|
2744
2882
|
assertProtectedMainWriteAllowed(options, 'setup');
|
|
2745
2883
|
const installPayload = runInstallInternal(options);
|
|
2884
|
+
installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)));
|
|
2746
2885
|
printOperations('Setup/install', installPayload, options.dryRun);
|
|
2747
2886
|
|
|
2748
2887
|
const fixPayload = runFixInternal({
|
package/package.json
CHANGED
|
@@ -23,6 +23,11 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
|
|
|
23
23
|
is_codex_session=1
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
+
is_vscode_git_context=0
|
|
27
|
+
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" || "${TERM_PROGRAM:-}" == "vscode" ]]; then
|
|
28
|
+
is_vscode_git_context=1
|
|
29
|
+
fi
|
|
30
|
+
|
|
26
31
|
protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
27
32
|
if [[ -z "$protected_branches_raw" ]]; then
|
|
28
33
|
protected_branches_raw="dev main master"
|
|
@@ -106,6 +111,10 @@ MSG
|
|
|
106
111
|
fi
|
|
107
112
|
|
|
108
113
|
if [[ "$is_protected_branch" == "1" ]]; then
|
|
114
|
+
if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
109
118
|
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
|
|
110
119
|
exit 0
|
|
111
120
|
fi
|
|
@@ -10,8 +10,9 @@ 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
|
-
|
|
14
|
-
|
|
13
|
+
is_codex_session=0
|
|
14
|
+
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
|
|
15
|
+
is_codex_session=1
|
|
15
16
|
fi
|
|
16
17
|
|
|
17
18
|
protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
|
|
@@ -43,6 +44,22 @@ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
43
44
|
done
|
|
44
45
|
|
|
45
46
|
if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
|
|
47
|
+
if [[ "$is_codex_session" == "1" ]]; then
|
|
48
|
+
{
|
|
49
|
+
echo "[guardex-preedit-guard] Codex push detected toward protected branch."
|
|
50
|
+
echo "[guardex-preedit-guard] Protected target(s): ${blocked_refs[*]}"
|
|
51
|
+
echo "[guardex-preedit-guard] Run Codex from an agent/* branch and merge via PR."
|
|
52
|
+
echo
|
|
53
|
+
echo "Temporary bypass (not recommended):"
|
|
54
|
+
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
|
|
55
|
+
} >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [[ "$is_vscode_git_context" == "1" ]]; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
46
63
|
{
|
|
47
64
|
echo "[agent-branch-guard] Push to protected branch blocked outside VS Code Git context."
|
|
48
65
|
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
|
|
@@ -26,6 +26,7 @@ LOCK_FILE_RELATIVE = Path('.omx/state/agent-file-locks.json')
|
|
|
26
26
|
CRITICAL_GUARDRAIL_PATHS = {
|
|
27
27
|
'AGENTS.md',
|
|
28
28
|
'.githooks/pre-commit',
|
|
29
|
+
'.githooks/pre-push',
|
|
29
30
|
'scripts/agent-branch-start.sh',
|
|
30
31
|
'scripts/agent-branch-finish.sh',
|
|
31
32
|
'scripts/agent-file-locks.py',
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
INTERVAL_SECONDS="${MUSAFETY_REVIEW_BOT_INTERVAL_SECONDS:-30}"
|
|
5
|
+
AGENT_NAME="${MUSAFETY_REVIEW_BOT_AGENT_NAME:-guardex-review-bot}"
|
|
6
|
+
TASK_PREFIX="${MUSAFETY_REVIEW_BOT_TASK_PREFIX:-review-merge}"
|
|
7
|
+
STATE_FILE="${MUSAFETY_REVIEW_BOT_STATE_FILE:-}"
|
|
8
|
+
BASE_BRANCH="${MUSAFETY_REVIEW_BOT_BASE_BRANCH:-}"
|
|
9
|
+
ONLY_PR="${MUSAFETY_REVIEW_BOT_ONLY_PR:-}"
|
|
10
|
+
RETRY_FAILED_RAW="${MUSAFETY_REVIEW_BOT_RETRY_FAILED:-false}"
|
|
11
|
+
INCLUDE_DRAFT_RAW="${MUSAFETY_REVIEW_BOT_INCLUDE_DRAFT:-false}"
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<'USAGE'
|
|
15
|
+
Usage: bash scripts/review-bot-watch.sh [options]
|
|
16
|
+
|
|
17
|
+
Continuously monitor GitHub pull requests targeting a base branch and dispatch
|
|
18
|
+
one Codex-agent task per newly opened/updated PR.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--base <branch> Base branch to watch (default: current branch)
|
|
22
|
+
--interval <seconds> Poll interval (default: 30)
|
|
23
|
+
--agent <name> Agent name for codex-agent (default: guardex-review-bot)
|
|
24
|
+
--task-prefix <prefix> Task prefix for codex-agent branches (default: review-merge)
|
|
25
|
+
--state-file <path> State file path (default: .omx/state/review-bot-watch-<base>.tsv)
|
|
26
|
+
--only-pr <number> Watch only one PR number
|
|
27
|
+
--include-draft Include draft PRs
|
|
28
|
+
--retry-failed Retry PRs that previously failed even when SHA is unchanged
|
|
29
|
+
--once Run one poll cycle and exit
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
|
|
32
|
+
Environment overrides:
|
|
33
|
+
MUSAFETY_REVIEW_BOT_PROMPT_APPEND Additional instructions appended to each Codex prompt
|
|
34
|
+
USAGE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
normalize_bool() {
|
|
38
|
+
local raw="${1:-}"
|
|
39
|
+
local fallback="${2:-0}"
|
|
40
|
+
case "$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" in
|
|
41
|
+
1|true|yes|on) printf '1' ;;
|
|
42
|
+
0|false|no|off) printf '0' ;;
|
|
43
|
+
'') printf '%s' "$fallback" ;;
|
|
44
|
+
*) printf '%s' "$fallback" ;;
|
|
45
|
+
esac
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ONCE=0
|
|
49
|
+
|
|
50
|
+
while [[ $# -gt 0 ]]; do
|
|
51
|
+
case "$1" in
|
|
52
|
+
--base)
|
|
53
|
+
BASE_BRANCH="${2:-}"
|
|
54
|
+
shift 2
|
|
55
|
+
;;
|
|
56
|
+
--interval)
|
|
57
|
+
INTERVAL_SECONDS="${2:-}"
|
|
58
|
+
shift 2
|
|
59
|
+
;;
|
|
60
|
+
--agent)
|
|
61
|
+
AGENT_NAME="${2:-}"
|
|
62
|
+
shift 2
|
|
63
|
+
;;
|
|
64
|
+
--task-prefix)
|
|
65
|
+
TASK_PREFIX="${2:-}"
|
|
66
|
+
shift 2
|
|
67
|
+
;;
|
|
68
|
+
--state-file)
|
|
69
|
+
STATE_FILE="${2:-}"
|
|
70
|
+
shift 2
|
|
71
|
+
;;
|
|
72
|
+
--only-pr)
|
|
73
|
+
ONLY_PR="${2:-}"
|
|
74
|
+
shift 2
|
|
75
|
+
;;
|
|
76
|
+
--retry-failed)
|
|
77
|
+
RETRY_FAILED_RAW="true"
|
|
78
|
+
shift
|
|
79
|
+
;;
|
|
80
|
+
--include-draft)
|
|
81
|
+
INCLUDE_DRAFT_RAW="true"
|
|
82
|
+
shift
|
|
83
|
+
;;
|
|
84
|
+
--once)
|
|
85
|
+
ONCE=1
|
|
86
|
+
shift
|
|
87
|
+
;;
|
|
88
|
+
-h|--help)
|
|
89
|
+
usage
|
|
90
|
+
exit 0
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
echo "[review-bot-watch] Unknown option: $1" >&2
|
|
94
|
+
usage >&2
|
|
95
|
+
exit 1
|
|
96
|
+
;;
|
|
97
|
+
esac
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
RETRY_FAILED="$(normalize_bool "$RETRY_FAILED_RAW" "0")"
|
|
101
|
+
INCLUDE_DRAFT="$(normalize_bool "$INCLUDE_DRAFT_RAW" "0")"
|
|
102
|
+
|
|
103
|
+
if [[ ! "$INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || [[ "$INTERVAL_SECONDS" -lt 5 ]]; then
|
|
104
|
+
echo "[review-bot-watch] --interval must be an integer >= 5 seconds." >&2
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [[ -n "$ONLY_PR" ]] && [[ ! "$ONLY_PR" =~ ^[0-9]+$ ]]; then
|
|
109
|
+
echo "[review-bot-watch] --only-pr must be a numeric PR id." >&2
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
114
|
+
echo "[review-bot-watch] Not inside a git repository." >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
118
|
+
|
|
119
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
120
|
+
BASE_BRANCH="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
121
|
+
fi
|
|
122
|
+
if [[ -z "$BASE_BRANCH" || "$BASE_BRANCH" == "HEAD" ]]; then
|
|
123
|
+
BASE_BRANCH="main"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
127
|
+
echo "[review-bot-watch] Missing GitHub CLI (gh)." >&2
|
|
128
|
+
echo "[review-bot-watch] Install gh and run: gh auth login" >&2
|
|
129
|
+
exit 127
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
133
|
+
echo "[review-bot-watch] Missing Codex CLI command: codex" >&2
|
|
134
|
+
exit 127
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
if [[ ! -x "$repo_root/scripts/codex-agent.sh" ]]; then
|
|
138
|
+
echo "[review-bot-watch] Missing scripts/codex-agent.sh. Run: gx setup" >&2
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
143
|
+
echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
sanitize_slug() {
|
|
148
|
+
local raw="$1"
|
|
149
|
+
local fallback="$2"
|
|
150
|
+
local slug
|
|
151
|
+
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
|
|
152
|
+
if [[ -z "$slug" ]]; then
|
|
153
|
+
slug="$fallback"
|
|
154
|
+
fi
|
|
155
|
+
printf '%s' "$slug"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
base_slug="$(sanitize_slug "$BASE_BRANCH" "base")"
|
|
159
|
+
if [[ -z "$STATE_FILE" ]]; then
|
|
160
|
+
STATE_FILE="$repo_root/.omx/state/review-bot-watch-${base_slug}.tsv"
|
|
161
|
+
fi
|
|
162
|
+
mkdir -p "$(dirname "$STATE_FILE")"
|
|
163
|
+
|
|
164
|
+
declare -A LAST_SHA
|
|
165
|
+
|
|
166
|
+
declare -A LAST_STATUS
|
|
167
|
+
|
|
168
|
+
load_state() {
|
|
169
|
+
if [[ ! -f "$STATE_FILE" ]]; then
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
while IFS=$'\t' read -r pr sha status updated_at; do
|
|
173
|
+
if [[ -z "${pr:-}" ]] || [[ "${pr:0:1}" == "#" ]]; then
|
|
174
|
+
continue
|
|
175
|
+
fi
|
|
176
|
+
LAST_SHA["$pr"]="$sha"
|
|
177
|
+
LAST_STATUS["$pr"]="$status"
|
|
178
|
+
done < "$STATE_FILE"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
save_state() {
|
|
182
|
+
{
|
|
183
|
+
echo "# pr\thead_sha\tstatus\tupdated_at"
|
|
184
|
+
for pr in "${!LAST_SHA[@]}"; do
|
|
185
|
+
printf '%s\t%s\t%s\t%s\n' "${pr}" "${LAST_SHA[$pr]}" "${LAST_STATUS[$pr]:-unknown}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
186
|
+
done | sort -n
|
|
187
|
+
} > "$STATE_FILE"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
build_prompt() {
|
|
191
|
+
local pr="$1"
|
|
192
|
+
local head_branch="$2"
|
|
193
|
+
local head_sha="$3"
|
|
194
|
+
local pr_title="$4"
|
|
195
|
+
local pr_url="$5"
|
|
196
|
+
|
|
197
|
+
cat <<PROMPT
|
|
198
|
+
You are the continuous PR review+merge Codex agent.
|
|
199
|
+
|
|
200
|
+
Target PR: #${pr}
|
|
201
|
+
URL: ${pr_url}
|
|
202
|
+
Title: ${pr_title}
|
|
203
|
+
Base branch: ${BASE_BRANCH}
|
|
204
|
+
Head branch: ${head_branch}
|
|
205
|
+
Head SHA: ${head_sha}
|
|
206
|
+
|
|
207
|
+
Strict task:
|
|
208
|
+
1) Review ONLY this PR's changes using gh CLI context (gh pr view ${pr}, gh pr diff ${pr}).
|
|
209
|
+
2) If fixes are needed, implement them in your sandbox branch, run verification (at minimum npm test when available), and push your sandbox branch.
|
|
210
|
+
3) When the PR is ready and checks are green, merge this PR into ${BASE_BRANCH} with:
|
|
211
|
+
gh pr merge ${pr} --squash --delete-branch
|
|
212
|
+
4) If merge is blocked, explain the blocker and exit non-zero.
|
|
213
|
+
5) Do not touch unrelated PRs.
|
|
214
|
+
PROMPT
|
|
215
|
+
|
|
216
|
+
if [[ -n "${MUSAFETY_REVIEW_BOT_PROMPT_APPEND:-}" ]]; then
|
|
217
|
+
printf '\n%s\n' "${MUSAFETY_REVIEW_BOT_PROMPT_APPEND}"
|
|
218
|
+
fi
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
list_open_prs() {
|
|
222
|
+
gh pr list \
|
|
223
|
+
--state open \
|
|
224
|
+
--base "$BASE_BRANCH" \
|
|
225
|
+
--json number,headRefName,headRefOid,isDraft,title,url \
|
|
226
|
+
--jq '.[] | "\(.number)\t\(.headRefName)\t\(.headRefOid)\t\(.isDraft)\t\(.title | gsub("\\t"; " "))\t\(.url)"'
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
should_process_pr() {
|
|
230
|
+
local pr="$1"
|
|
231
|
+
local sha="$2"
|
|
232
|
+
|
|
233
|
+
local prev_sha="${LAST_SHA[$pr]:-}"
|
|
234
|
+
local prev_status="${LAST_STATUS[$pr]:-}"
|
|
235
|
+
|
|
236
|
+
if [[ -z "$prev_sha" ]]; then
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
if [[ "$prev_sha" != "$sha" ]]; then
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if [[ "$prev_status" == "failed" && "$RETRY_FAILED" == "1" ]]; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
return 1
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
process_one_pr() {
|
|
252
|
+
local pr="$1"
|
|
253
|
+
local head_branch="$2"
|
|
254
|
+
local sha="$3"
|
|
255
|
+
local title="$4"
|
|
256
|
+
local url="$5"
|
|
257
|
+
|
|
258
|
+
local prompt
|
|
259
|
+
prompt="$(build_prompt "$pr" "$head_branch" "$sha" "$title" "$url")"
|
|
260
|
+
|
|
261
|
+
local task_name="${TASK_PREFIX}-pr-${pr}"
|
|
262
|
+
|
|
263
|
+
echo "[review-bot-watch] Dispatching Codex agent for PR #${pr} (${head_branch})"
|
|
264
|
+
set +e
|
|
265
|
+
bash "$repo_root/scripts/codex-agent.sh" \
|
|
266
|
+
--task "$task_name" \
|
|
267
|
+
--agent "$AGENT_NAME" \
|
|
268
|
+
--base "$BASE_BRANCH" \
|
|
269
|
+
-- exec "$prompt"
|
|
270
|
+
local exit_code="$?"
|
|
271
|
+
set -e
|
|
272
|
+
|
|
273
|
+
LAST_SHA["$pr"]="$sha"
|
|
274
|
+
if [[ "$exit_code" -eq 0 ]]; then
|
|
275
|
+
LAST_STATUS["$pr"]="success"
|
|
276
|
+
echo "[review-bot-watch] PR #${pr}: success"
|
|
277
|
+
else
|
|
278
|
+
LAST_STATUS["$pr"]="failed"
|
|
279
|
+
echo "[review-bot-watch] PR #${pr}: failed (exit=${exit_code})" >&2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
save_state
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
load_state
|
|
286
|
+
|
|
287
|
+
echo "[review-bot-watch] Starting monitor"
|
|
288
|
+
echo "[review-bot-watch] Base branch : ${BASE_BRANCH}"
|
|
289
|
+
echo "[review-bot-watch] Interval : ${INTERVAL_SECONDS}s"
|
|
290
|
+
echo "[review-bot-watch] State file : ${STATE_FILE}"
|
|
291
|
+
if [[ -n "$ONLY_PR" ]]; then
|
|
292
|
+
echo "[review-bot-watch] Only PR : #${ONLY_PR}"
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
trap 'echo "[review-bot-watch] Stopped."; exit 0' INT TERM
|
|
296
|
+
|
|
297
|
+
while true; do
|
|
298
|
+
found=0
|
|
299
|
+
while IFS=$'\t' read -r pr head_branch sha is_draft title url; do
|
|
300
|
+
if [[ -z "${pr:-}" ]]; then
|
|
301
|
+
continue
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
found=1
|
|
305
|
+
|
|
306
|
+
if [[ -n "$ONLY_PR" && "$pr" != "$ONLY_PR" ]]; then
|
|
307
|
+
continue
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
if [[ "$INCLUDE_DRAFT" != "1" && "$is_draft" == "true" ]]; then
|
|
311
|
+
continue
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
if ! should_process_pr "$pr" "$sha"; then
|
|
315
|
+
continue
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
process_one_pr "$pr" "$head_branch" "$sha" "$title" "$url"
|
|
319
|
+
done < <(list_open_prs || true)
|
|
320
|
+
|
|
321
|
+
if [[ "$found" -eq 0 ]]; then
|
|
322
|
+
echo "[review-bot-watch] No open PRs for base '${BASE_BRANCH}'."
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if [[ "$ONCE" -eq 1 ]]; then
|
|
326
|
+
break
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
sleep "$INTERVAL_SECONDS"
|
|
330
|
+
done
|