Skip to content

Commit 5efc609

Browse files
NagyViktNagyVikt
andauthored
Require setup tooling before guarded workflows (#520)
Guardex-managed repos now depend on RTK and fff-mcp for compact command output and MCP-backed file search, so setup, doctor, and status expose them with the same required-tool path as gh. The AGENTS template also records the fff search rule so generated workspaces inherit the contract. Constraint: Repo guidance requires RTK command routing and fff MCP file search in managed agent workspaces. Rejected: Keep rtk and fff-mcp as optional companion tools | missing tools would leave generated guidance unenforced until a later runtime failure. Confidence: high Scope-risk: narrow Directive: Keep required system tooling surfaced through detectRequiredSystemTools so setup, status, and doctor stay aligned. Tested: node --check src/context.js Tested: node --check src/cli/main.js Tested: rtk test node --test test/setup.test.js test/status.test.js test/doctor.test.js test/prompt.test.js Tested: openspec validate --specs Tested: node bin/multiagent-safety.js setup --target . --dry-run --no-global-install --current Not-tested: Full npm test remains red in unrelated agents/cockpit/merge/vscode groups per handoff. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent c1d739d commit 5efc609

7 files changed

Lines changed: 81 additions & 14 deletions

File tree

src/cli/main.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,6 +2400,10 @@ function doctor(rawArgs) {
24002400
target: topRepoRoot,
24012401
};
24022402

2403+
if (!singleRepoOptions.json) {
2404+
printRequiredSystemToolStatus();
2405+
}
2406+
24032407
const blocked = protectedBaseWriteBlock(singleRepoOptions, { requireBootstrap: false });
24042408
if (blocked) {
24052409
doctorModule.runDoctorInSandbox(singleRepoOptions, blocked, {
@@ -3007,18 +3011,7 @@ function setup(rawArgs) {
30073011

30083012
maybePromptInstallVscodeExtension(options);
30093013

3010-
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
3011-
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
3012-
if (missingSystemTools.length === 0) {
3013-
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
3014-
} else {
3015-
const names = missingSystemTools.map((tool) => tool.name).join(', ');
3016-
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${names}`);
3017-
for (const tool of missingSystemTools) {
3018-
const reasonText = tool.reason ? ` (${tool.reason})` : '';
3019-
console.log(`[${TOOL_NAME}] Install ${tool.name}: ${tool.installHint}${reasonText}`);
3020-
}
3021-
}
3014+
printRequiredSystemToolStatus();
30223015

30233016
const topRepoRoot = resolveRepoRoot(options.target);
30243017
const discoveredRepos = options.recursive
@@ -3132,6 +3125,22 @@ function setup(rawArgs) {
31323125
}
31333126
}
31343127

3128+
function printRequiredSystemToolStatus() {
3129+
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
3130+
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
3131+
if (missingSystemTools.length === 0) {
3132+
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
3133+
return;
3134+
}
3135+
3136+
const names = missingSystemTools.map((tool) => tool.name).join(', ');
3137+
console.log(`[${TOOL_NAME}] ⚠️ Missing required system tool(s): ${names}`);
3138+
for (const tool of missingSystemTools) {
3139+
const reasonText = tool.reason ? ` (${tool.reason})` : '';
3140+
console.log(`[${TOOL_NAME}] Install ${tool.name}: ${tool.installHint}${reasonText}`);
3141+
}
3142+
}
3143+
31353144
function ensureMainBranch(repoRoot) {
31363145
const branchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
31373146
if (branchResult.status !== 0) {

src/context.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,27 @@ function resolveGithubCliBin(env = process.env) {
7575
}
7676

7777
const GH_BIN = resolveGithubCliBin();
78+
const RTK_BIN = process.env.GUARDEX_RTK_BIN || 'rtk';
79+
const FFF_MCP_BIN = process.env.GUARDEX_FFF_MCP_BIN || 'fff-mcp';
7880
const REQUIRED_SYSTEM_TOOLS = [
7981
{
8082
name: 'gh',
8183
displayName: GH_BIN === 'ghx' ? 'GitHub (ghx proxy)' : 'GitHub (gh)',
8284
command: GH_BIN,
8385
installHint: 'https://cli.github.com/',
8486
},
87+
{
88+
name: 'rtk',
89+
displayName: 'RTK (rtk)',
90+
command: RTK_BIN,
91+
installHint: 'Install RTK and ensure `rtk` is on PATH.',
92+
},
93+
{
94+
name: 'fff-mcp',
95+
displayName: 'FFF MCP (fff-mcp)',
96+
command: FFF_MCP_BIN,
97+
installHint: 'https://github.com/dmtrKovalenko/fff.nvim',
98+
},
8599
];
86100
const MAINTAINER_RELEASE_REPO = path.resolve(
87101
process.env.GUARDEX_RELEASE_REPO || PACKAGE_ROOT,

templates/AGENTS.multiagent-safety.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ GUARDEX_ON=1
2727
- Work from an `agent/*` branch and worktree, never directly on the protected base branch.
2828
- Claim files before edits.
2929
- Use Colony for coordination before falling back to OMX state/notepad.
30+
- Use the fff MCP tools for all file search operations instead of default tools.
3031
- Use OpenSpec for durable behavior contracts and change-driven work.
3132
- Keep outputs compact: less word, same proof.
3233
- 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.
147148
- Do not wrap machine-readable commands with RTK when code parses stdout (`--porcelain`, `--json`, NUL-delimited output, or exact stdout contracts).
148149
- If `rtk` is missing, use raw commands and summarize only meaningful lines.
149150

151+
### FFF file search
152+
153+
Use the fff MCP tools for all file search operations instead of default tools.
154+
155+
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.
156+
150157
### Caveman style
151158

152159
Commentary and progress updates use smart-caveman `ultra` by default:

test/doctor.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ const {
6363

6464
defineSpawnSuite('doctor integration suite', () => {
6565

66+
test('doctor warns when required system tool dependencies are missing', () => {
67+
const repoDir = initRepo();
68+
69+
const result = runNodeWithEnv(['doctor', '--target', repoDir], repoDir, {
70+
GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test',
71+
GUARDEX_FFF_MCP_BIN: 'fff-mcp-command-not-found-for-test',
72+
});
73+
74+
assert.equal(result.status, 0, result.stderr || result.stdout);
75+
assert.match(result.stdout, /Missing required system tool\(s\): rtk, fff-mcp/);
76+
assert.match(result.stdout, /Install rtk: Install RTK and ensure `rtk` is on PATH\./);
77+
assert.match(result.stdout, /Install fff-mcp: https:\/\/github\.com\/dmtrKovalenko\/fff\.nvim/);
78+
});
79+
6680
test('doctor --force <managed-path> rewrites only the named managed shim', () => {
6781
const repoDir = initRepo();
6882

test/prompt.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ test('prompt --snippet prints the managed AGENTS template with token budget and
164164
assert.match(result.stdout, /### RTK command compression/);
165165
assert.match(result.stdout, /rtk git status/);
166166
assert.match(result.stdout, /Do not wrap machine-readable commands with RTK/);
167+
assert.match(result.stdout, /### FFF file search/);
168+
assert.match(result.stdout, /Use the fff MCP tools for all file search operations instead of default tools\./);
167169
assert.match(result.stdout, /### Caveman style/);
168170
assert.match(result.stdout, /Answer order stays fixed: answer first, cause next, fix or next step last\./);
169171
});

test/setup.test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ exit 1
14141414
});
14151415

14161416

1417-
test('setup warns when gh dependency is missing', () => {
1417+
test('setup warns when required system tool dependencies are missing', () => {
14181418
const repoDir = initRepo();
14191419
const fakeHome = createGuardexCompanionHome({ cavekit: true, caveman: true });
14201420
const fakeNpm = createFakeNpmScript(`
@@ -1432,11 +1432,15 @@ exit 1
14321432
GUARDEX_NPM_BIN: fakeNpm,
14331433
GUARDEX_HOME_DIR: fakeHome,
14341434
GUARDEX_GH_BIN: 'gh-command-not-found-for-test',
1435+
GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test',
1436+
GUARDEX_FFF_MCP_BIN: 'fff-mcp-command-not-found-for-test',
14351437
});
14361438

14371439
assert.equal(result.status, 0, result.stderr || result.stdout);
1438-
assert.match(result.stdout, /Missing required system tool\(s\): gh/);
1440+
assert.match(result.stdout, /Missing required system tool\(s\): gh, rtk, fff-mcp/);
14391441
assert.match(result.stdout, /https:\/\/cli\.github\.com\//);
1442+
assert.match(result.stdout, /Install rtk: Install RTK and ensure `rtk` is on PATH\./);
1443+
assert.match(result.stdout, /Install fff-mcp: https:\/\/github\.com\/dmtrKovalenko\/fff\.nvim/);
14401444
});
14411445

14421446
});

test/status.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,23 @@ test('status reports gh dependency as inactive when gh is unavailable', () => {
562562
assert.equal(ghService.status, 'inactive');
563563
});
564564

565+
test('status reports rtk and fff-mcp dependencies as inactive when unavailable', () => {
566+
const repoDir = initRepo();
567+
const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, {
568+
GUARDEX_RTK_BIN: 'rtk-command-not-found-for-test',
569+
GUARDEX_FFF_MCP_BIN: 'fff-mcp-command-not-found-for-test',
570+
});
571+
572+
assert.equal(result.status, 0, result.stderr || result.stdout);
573+
const payload = JSON.parse(result.stdout);
574+
const rtkService = payload.services.find((service) => service.name === 'rtk');
575+
const fffService = payload.services.find((service) => service.name === 'fff-mcp');
576+
assert.ok(rtkService, 'rtk service should be included in status payload');
577+
assert.ok(fffService, 'fff-mcp service should be included in status payload');
578+
assert.equal(rtkService.status, 'inactive');
579+
assert.equal(fffService.status, 'inactive');
580+
});
581+
565582

566583
test('unknown command suggests nearest valid command', () => {
567584
const repoDir = initRepo();

0 commit comments

Comments
 (0)