Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ gx sync --check
gx sync

# continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
bash scripts/review-bot-watch.sh --interval 30
gx review --interval 30

# cleanup merged agent branches and hide clean stale agent worktrees
gx cleanup
Expand All @@ -126,7 +126,7 @@ gx report scorecard --repo github.com/recodeecom/multiagent-safety
Run this in your local shell to keep watching PRs targeting the current branch (or `--base <branch>`):

```sh
bash scripts/review-bot-watch.sh --interval 30
gx review --interval 30
```

Useful flags:
Expand Down
60 changes: 52 additions & 8 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const SUGGESTIBLE_COMMANDS = [
'setup',
'init',
'doctor',
'review',
'report',
'copy-prompt',
'copy-commands',
Expand Down Expand Up @@ -161,8 +162,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
['version', 'Print GuardeX version'],
];
const AGENT_BOT_DESCRIPTIONS = [
['review', 'Monitor open PRs targeting current branch and dispatch codex-agent review flow'],
['start', 'bash scripts/review-bot-watch.sh --interval 30'],
['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
];

const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
Expand All @@ -184,7 +184,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
3) If setup reports warnings/errors, repair + re-check:
gx doctor

4) Confirm next safe agent workflow commands:
4) Optional: start continuous PR monitor from this repo:
gx review --interval 30

5) Confirm next safe agent workflow commands:
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
Expand All @@ -196,24 +199,25 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
Remove them explicitly when done:
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"

5) Optional: create OpenSpec planning workspace:
6) Optional: create OpenSpec planning workspace:
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"

6) Optional: protect extra branches:
7) Optional: protect extra branches:
gx protect add release staging

7) Optional: sync your current agent branch with latest base branch:
8) Optional: sync your current agent branch with latest base branch:
gx sync --check
gx sync

8) Optional (GitHub remote cleanup): enable:
9) Optional (GitHub remote cleanup): enable:
Settings -> General -> Pull Requests -> Automatically delete head branches
`;

const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
gh --version
gx setup
gx doctor
gx review --interval 30
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
Expand Down Expand Up @@ -361,6 +365,7 @@ NOTES
- ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
- ${TOOL_NAME} setup asks for Y/N approval before global installs
- ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
- For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
- In initialized repos, setup/install/fix block in-place writes on protected main by default
- doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
- agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
Expand All @@ -371,7 +376,8 @@ NOTES
console.log(`
[${TOOL_NAME}] No git repository detected in current directory.
[${TOOL_NAME}] Start from a repo root, or pass an explicit target:
${TOOL_NAME} setup --target <path-to-git-repo>`);
${TOOL_NAME} setup --target <path-to-git-repo>
${TOOL_NAME} doctor --target <path-to-git-repo>`);
}
}

Expand Down Expand Up @@ -1486,6 +1492,18 @@ function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
return { target, args: remaining };
}

function parseReviewArgs(rawArgs) {
const parsed = parseTargetFlag(rawArgs, process.cwd());
const passthroughArgs = [...parsed.args];
if (passthroughArgs[0] === 'start') {
passthroughArgs.shift();
}
return {
target: parsed.target,
passthroughArgs,
};
}

function parseReportArgs(rawArgs) {
const options = {
target: process.cwd(),
Expand Down Expand Up @@ -2926,6 +2944,27 @@ function doctor(rawArgs) {
setExitCodeFromScan(scanResult);
}

function review(rawArgs) {
const options = parseReviewArgs(rawArgs);
const repoRoot = resolveRepoRoot(options.target);
const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
if (!fs.existsSync(reviewScriptPath)) {
throw new Error(
`Missing review bot script: ${reviewScriptPath}\n` +
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
);
}

const result = run('bash', [reviewScriptPath, ...options.passthroughArgs], { cwd: repoRoot });
if (isSpawnFailure(result)) {
throw result.error;
}

if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
process.exitCode = typeof result.status === 'number' ? result.status : 1;
}

function report(rawArgs) {
const options = parseReportArgs(rawArgs);
const subcommand = options.subcommand || 'help';
Expand Down Expand Up @@ -3517,6 +3556,11 @@ function main() {
return;
}

if (command === 'review') {
review(rest);
return;
}

if (command === 'report') {
report(rest);
return;
Expand Down
38 changes: 36 additions & 2 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -990,12 +990,46 @@ test('default invocation runs non-mutating status output', () => {
assert.match(result.stdout, /COMMANDS\n\s+status\s+Show GuardeX CLI \+ service health without modifying files/);
assert.match(
result.stdout,
/AGENT BOT\n\s+review\s+Monitor open PRs targeting current branch and dispatch codex-agent review flow/,
/AGENT BOT\n\s+review\s+Start PR monitor \+ codex-agent review flow \(default interval: 30s\)/,
);
assert.match(result.stdout, /AGENT BOT[\s\S]*\n\s+start\s+bash scripts\/review-bot-watch\.sh --interval 30/);
assert.doesNotMatch(result.stdout, /AGENT BOT[\s\S]*\n\s+start\s+/);
assert.equal(fs.existsSync(path.join(repoDir, '.githooks', 'pre-commit')), false);
});

test('review command launches local review-bot script and accepts legacy start token', () => {
const repoDir = initRepo();
const scriptsDir = path.join(repoDir, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
const reviewScript = path.join(scriptsDir, 'review-bot-watch.sh');
const markerCwd = path.join(repoDir, '.review-bot-cwd');
const markerArgs = path.join(repoDir, '.review-bot-args');
fs.writeFileSync(
reviewScript,
'#!/usr/bin/env bash\n' +
'set -euo pipefail\n' +
`printf '%s\\n' \"$PWD\" > \"${markerCwd}\"\n` +
`printf '%s\\n' \"$*\" > \"${markerArgs}\"\n`,
'utf8',
);
fs.chmodSync(reviewScript, 0o755);

const result = runNode(['review', 'start', '--target', repoDir, '--interval', '45', '--once'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.readFileSync(markerCwd, 'utf8').trim(), repoDir);
assert.equal(fs.readFileSync(markerArgs, 'utf8').trim(), '--interval 45 --once');
});

test('review command explains setup + doctor steps when script is missing in target repo', () => {
const repoDir = initRepo();

const result = runNode(['review', '--target', repoDir], repoDir);
assert.equal(result.status, 1, result.stderr || result.stdout);
assert.match(
result.stderr,
new RegExp(`Run 'gx setup --target ${escapeRegexLiteral(repoDir)}' then 'gx doctor --target ${escapeRegexLiteral(repoDir)}'`),
);
});

test('status prints GitHub CLI service with friendly label', () => {
const repoDir = initRepo();
const fakeGh = createFakeGhScript(`
Expand Down
Loading