diff --git a/src/cli/main.js b/src/cli/main.js index c720600..8db9e1a 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -2400,6 +2400,10 @@ function doctor(rawArgs) { target: topRepoRoot, }; + if (!singleRepoOptions.json) { + printRequiredSystemToolStatus(); + } + const blocked = protectedBaseWriteBlock(singleRepoOptions, { requireBootstrap: false }); if (blocked) { doctorModule.runDoctorInSandbox(singleRepoOptions, blocked, { @@ -3007,18 +3011,7 @@ function setup(rawArgs) { maybePromptInstallVscodeExtension(options); - const requiredSystemTools = toolchainModule.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}`); - } - } + printRequiredSystemToolStatus(); const topRepoRoot = resolveRepoRoot(options.target); const discoveredRepos = options.recursive @@ -3132,6 +3125,22 @@ function setup(rawArgs) { } } +function printRequiredSystemToolStatus() { + const requiredSystemTools = toolchainModule.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(', ')}).`); + return; + } + + 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}`); + } +} + function ensureMainBranch(repoRoot) { const branchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true }); if (branchResult.status !== 0) { diff --git a/src/context.js b/src/context.js index 09524bc..e4f0468 100644 --- a/src/context.js +++ b/src/context.js @@ -75,6 +75,8 @@ function resolveGithubCliBin(env = process.env) { } const GH_BIN = resolveGithubCliBin(); +const RTK_BIN = process.env.GUARDEX_RTK_BIN || 'rtk'; +const FFF_MCP_BIN = process.env.GUARDEX_FFF_MCP_BIN || 'fff-mcp'; const REQUIRED_SYSTEM_TOOLS = [ { name: 'gh', @@ -82,6 +84,18 @@ const REQUIRED_SYSTEM_TOOLS = [ command: GH_BIN, installHint: 'https://cli.github.com/', }, + { + name: 'rtk', + displayName: 'RTK (rtk)', + command: RTK_BIN, + installHint: 'Install RTK and ensure `rtk` is on PATH.', + }, + { + name: 'fff-mcp', + displayName: 'FFF MCP (fff-mcp)', + command: FFF_MCP_BIN, + installHint: 'https://github.com/dmtrKovalenko/fff.nvim', + }, ]; const MAINTAINER_RELEASE_REPO = path.resolve( process.env.GUARDEX_RELEASE_REPO || PACKAGE_ROOT, diff --git a/templates/AGENTS.multiagent-safety.md b/templates/AGENTS.multiagent-safety.md index 0295bef..b9f0ea9 100644 --- a/templates/AGENTS.multiagent-safety.md +++ b/templates/AGENTS.multiagent-safety.md @@ -27,6 +27,7 @@ GUARDEX_ON=1 - Work from an `agent/*` branch and worktree, never directly on the protected base branch. - Claim files before edits. - Use Colony for coordination before falling back to OMX state/notepad. +- Use the fff MCP tools for all file search operations instead of default tools. - Use OpenSpec for durable behavior contracts and change-driven work. - Keep outputs compact: less word, same proof. - Commit, push, and open/update a PR for completed work unless the user explicitly says to keep it local. @@ -147,6 +148,12 @@ When `rtk` is available, prefer it for noisy shell discovery and verification. - Do not wrap machine-readable commands with RTK when code parses stdout (`--porcelain`, `--json`, NUL-delimited output, or exact stdout contracts). - If `rtk` is missing, use raw commands and summarize only meaningful lines. +### FFF file search + +Use the fff MCP tools for all file search operations instead of default tools. + +If `fff-mcp` is missing, run `gx setup` or `gx doctor` and follow the required system tool warning before falling back to repo-local search. + ### Caveman style Commentary and progress updates use smart-caveman `ultra` by default: diff --git a/test/doctor.test.js b/test/doctor.test.js index 1cf1c01..c4f3ca6 100644 --- a/test/doctor.test.js +++ b/test/doctor.test.js @@ -63,6 +63,20 @@ const { defineSpawnSuite('doctor integration suite', () => { +test('doctor warns when required system tool dependencies are missing', () => { + const repoDir = initRepo(); + + const result = runNodeWithEnv(['doctor', '--target', repoDir], repoDir, { + GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test', + GUARDEX_FFF_MCP_BIN: 'fff-mcp-command-not-found-for-test', + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /Missing required system tool\(s\): rtk, fff-mcp/); + assert.match(result.stdout, /Install rtk: Install RTK and ensure `rtk` is on PATH\./); + assert.match(result.stdout, /Install fff-mcp: https:\/\/github\.com\/dmtrKovalenko\/fff\.nvim/); +}); + test('doctor --force rewrites only the named managed shim', () => { const repoDir = initRepo(); diff --git a/test/prompt.test.js b/test/prompt.test.js index fd9ad65..72b9bc1 100644 --- a/test/prompt.test.js +++ b/test/prompt.test.js @@ -164,6 +164,8 @@ test('prompt --snippet prints the managed AGENTS template with token budget and assert.match(result.stdout, /### RTK command compression/); assert.match(result.stdout, /rtk git status/); assert.match(result.stdout, /Do not wrap machine-readable commands with RTK/); + assert.match(result.stdout, /### FFF file search/); + assert.match(result.stdout, /Use the fff MCP tools for all file search operations instead of default tools\./); assert.match(result.stdout, /### Caveman style/); assert.match(result.stdout, /Answer order stays fixed: answer first, cause next, fix or next step last\./); }); diff --git a/test/setup.test.js b/test/setup.test.js index dcf0352..d88e7d7 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -1414,7 +1414,7 @@ exit 1 }); -test('setup warns when gh dependency is missing', () => { +test('setup warns when required system tool dependencies are missing', () => { const repoDir = initRepo(); const fakeHome = createGuardexCompanionHome({ cavekit: true, caveman: true }); const fakeNpm = createFakeNpmScript(` @@ -1432,11 +1432,15 @@ exit 1 GUARDEX_NPM_BIN: fakeNpm, GUARDEX_HOME_DIR: fakeHome, GUARDEX_GH_BIN: 'gh-command-not-found-for-test', + GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test', + GUARDEX_FFF_MCP_BIN: 'fff-mcp-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, /Missing required system tool\(s\): gh, rtk, fff-mcp/); assert.match(result.stdout, /https:\/\/cli\.github\.com\//); + assert.match(result.stdout, /Install rtk: Install RTK and ensure `rtk` is on PATH\./); + assert.match(result.stdout, /Install fff-mcp: https:\/\/github\.com\/dmtrKovalenko\/fff\.nvim/); }); }); diff --git a/test/status.test.js b/test/status.test.js index 2a2adb7..cc7c562 100644 --- a/test/status.test.js +++ b/test/status.test.js @@ -562,6 +562,23 @@ test('status reports gh dependency as inactive when gh is unavailable', () => { assert.equal(ghService.status, 'inactive'); }); +test('status reports rtk and fff-mcp dependencies as inactive when unavailable', () => { + const repoDir = initRepo(); + const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, { + GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test', + GUARDEX_FFF_MCP_BIN: 'fff-mcp-command-not-found-for-test', + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = JSON.parse(result.stdout); + const rtkService = payload.services.find((service) => service.name === 'rtk'); + const fffService = payload.services.find((service) => service.name === 'fff-mcp'); + assert.ok(rtkService, 'rtk service should be included in status payload'); + assert.ok(fffService, 'fff-mcp service should be included in status payload'); + assert.equal(rtkService.status, 'inactive'); + assert.equal(fffService.status, 'inactive'); +}); + test('unknown command suggests nearest valid command', () => { const repoDir = initRepo();