Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ gx report scorecard --repo github.com/recodeecom/multiagent-safety
- No command defaults to `gx status`.
- `gx init` is alias of `gx setup`.
- Setup/doctor can install missing global OMX/OpenSpec/codex-auth with explicit Y/N confirmation.
- `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
- Interactive self-update prompt defaults to **No** (`[y/N]`).
- In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
Expand All @@ -145,6 +146,19 @@ Stored in git config key:
multiagent.protectedBranches
```

## Companion dependency: GitHub CLI (`gh`)

GuardeX PR/merge automation depends on GitHub CLI (`gh`), including
`agent-branch-finish.sh` PR flows and `codex-agent.sh` auto-finish behavior.

Install + verify:

```sh
# install guide: https://cli.github.com/
gh --version
gh auth status
```

## Companion dependency: `codex-auth` account switcher

For multi-identity Codex workflows, GuardeX pairs with
Expand Down
67 changes: 64 additions & 3 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const GLOBAL_TOOLCHAIN_PACKAGES = [
'@fission-ai/openspec',
'@imdeadpool/codex-account-switcher',
];
const GH_BIN = process.env.MUSAFETY_GH_BIN || 'gh';
const REQUIRED_SYSTEM_TOOLS = [
{
name: 'gh',
command: GH_BIN,
installHint: 'https://cli.github.com/',
},
];
const MAINTAINER_RELEASE_REPO = path.resolve(
process.env.MUSAFETY_RELEASE_REPO || '/tmp/multiagent-safety',
);
Expand Down Expand Up @@ -141,10 +149,12 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
gx setup
# alias: gx init

- Setup detects global OMX/OpenSpec/codex-auth first.
- Setup detects global OMX/OpenSpec/codex-auth npm packages first.
- If one is missing and setup asks for approval, reply explicitly:
- y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
- n = skip global installs
- Setup also checks GitHub CLI (gh), required for PR/merge automation.
- If gh is missing: install it from https://cli.github.com/ and rerun gx setup.

3) If setup reports warnings/errors, repair + re-check:
gx doctor
Expand Down Expand Up @@ -176,6 +186,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
`;

const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
gh --version
gx setup
gx doctor
bash scripts/codex-agent.sh "task" "agent-name"
Expand Down Expand Up @@ -297,6 +308,7 @@ NOTES
- Short alias: ${SHORT_TOOL_NAME}
- ${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
- 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 Down Expand Up @@ -2029,6 +2041,26 @@ function detectGlobalToolchainPackages() {
return { ok: true, installed, missing };
}

function detectRequiredSystemTools() {
const services = [];
for (const tool of REQUIRED_SYSTEM_TOOLS) {
const result = run(tool.command, ['--version']);
const active = result.status === 0;
const rawReason = result.error && result.error.code
? result.error.code
: (result.stderr || '').trim();
const reason = rawReason.split('\n')[0] || '';
services.push({
name: tool.name,
command: tool.command,
installHint: tool.installHint,
status: active ? 'active' : 'inactive',
reason,
});
}
return services;
}

function askGlobalInstallForMissing(options, missingPackages) {
const approval = resolveGlobalInstallApproval(options);
if (!approval.approved) {
Expand Down Expand Up @@ -2358,7 +2390,7 @@ function status(rawArgs) {
});

const toolchain = detectGlobalToolchainPackages();
const services = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
if (!toolchain.ok) {
return { name: pkg, status: 'unknown' };
}
Expand All @@ -2367,6 +2399,14 @@ function status(rawArgs) {
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
};
});
const requiredSystemTools = detectRequiredSystemTools();
const services = [
...npmServices,
...requiredSystemTools.map((tool) => ({
name: tool.name,
status: tool.status,
})),
];

const targetPath = path.resolve(options.target);
const inGitRepo = isGitRepo(targetPath);
Expand Down Expand Up @@ -2414,6 +2454,15 @@ function status(rawArgs) {
for (const service of services) {
console.log(` - ${statusDot(service.status)} ${service.name}: ${service.status}`);
}
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
if (missingSystemTools.length > 0) {
const tools = missingSystemTools.map((tool) => tool.name).join(', ');
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${tools}`);
for (const tool of missingSystemTools) {
const reasonText = tool.reason ? ` (${tool.reason})` : '';
console.log(` - install ${tool.name}: ${tool.installHint}${reasonText}`);
}
}

if (!scanResult) {
console.log(
Expand Down Expand Up @@ -2666,7 +2715,7 @@ function setup(rawArgs) {
`[${TOOL_NAME}] ✅ Global tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
);
} else if (globalInstallStatus.status === 'already-installed') {
console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec/codex-auth global tools already installed. Skipping.`);
console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec/codex-auth npm global tools already installed. Skipping.`);
} else if (globalInstallStatus.status === 'failed') {
console.log(
`[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
Expand All @@ -2679,6 +2728,18 @@ function setup(rawArgs) {
`Use --yes-global-install to force or run interactively for Y/N prompt.`,
);
}
const requiredSystemTools = detectRequiredSystemTools();
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
if (missingSystemTools.length === 0) {
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
} else {
const names = missingSystemTools.map((tool) => tool.name).join(', ');
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${names}`);
for (const tool of missingSystemTools) {
const reasonText = tool.reason ? ` (${tool.reason})` : '';
console.log(`[${TOOL_NAME}] Install ${tool.name}: ${tool.installHint}${reasonText}`);
}
}

assertProtectedMainWriteAllowed(options, 'setup');
const installPayload = runInstallInternal(options);
Expand Down
37 changes: 37 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,7 @@ test('copy-commands outputs command-only checklist', () => {
const result = runNode(['copy-commands'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /^npm i -g @imdeadpool\/guardex/m);
assert.match(result.stdout, /^gh --version/m);
assert.match(result.stdout, /gx setup/);
assert.match(result.stdout, /gx doctor/);
assert.match(result.stdout, /scripts\/agent-file-locks.py claim/);
Expand Down Expand Up @@ -1911,6 +1912,42 @@ exit 1
assert.equal(args, 'i -g @fission-ai/openspec @imdeadpool/codex-account-switcher');
});

test('status reports gh dependency as inactive when gh is unavailable', () => {
const repoDir = initRepo();
const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, {
MUSAFETY_GH_BIN: 'gh-command-not-found-for-test',
});

assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
const ghService = payload.services.find((service) => service.name === 'gh');
assert.ok(ghService, 'gh service should be included in status payload');
assert.equal(ghService.status, 'inactive');
});

test('setup warns when gh dependency is missing', () => {
const repoDir = initRepo();
const fakeNpm = createFakeNpmScript(`
if [[ "$1" == "list" ]]; then
cat <<'JSON'
{"dependencies":{"oh-my-codex":{"version":"1.0.0"},"@fission-ai/openspec":{"version":"1.0.0"},"@imdeadpool/codex-account-switcher":{"version":"1.0.0"}}}
JSON
exit 0
fi
echo "unexpected npm args: $*" >&2
exit 1
`);

const result = runNodeWithEnv(['setup', '--target', repoDir, '--yes-global-install'], repoDir, {
MUSAFETY_NPM_BIN: fakeNpm,
MUSAFETY_GH_BIN: 'gh-command-not-found-for-test',
});

assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Missing required system tool\(s\): gh/);
assert.match(result.stdout, /https:\/\/cli\.github\.com\//);
});

test('worktree prune keeps merged agent worktrees/branches unless delete flags are set', () => {
const repoDir = initRepo();
let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
Expand Down
Loading