diff --git a/README.md b/README.md index 8115108..ee2923d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index a6e4b7b..50d27fa 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -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', ); @@ -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 @@ -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" @@ -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 @@ -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) { @@ -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' }; } @@ -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); @@ -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( @@ -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` + @@ -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); diff --git a/test/install.test.js b/test/install.test.js index 9942ccb..bcd65c1 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -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/); @@ -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);