fix(cli): dynamically discover and install agent skills#270
fix(cli): dynamically discover and install agent skills#270magyargergo merged 8 commits intoabhigyanpatwari:mainfrom
Conversation
This fixes an issue where the `SKILL_NAMES` array was hardcoded, preventing newly added skills from being installed when running `gitnexus setup`. Fixes abhigyanpatwari#269 Made-with: Cursor
|
Someone is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
|
Hey @cnighut , Could you please merge origin/master into this branch? 🙏 Thanks! |
Done @magyargergo! |
|
@claude review this code and recommend a more efficient way of traversing and finding md files like glob for example. |
|
Claude finished @magyargergo's task in 1m 34s —— View job Code Review: Glob-based skill discovery
Current implementation analysisThe Redundant
// Line 258 — already has type info ✓
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
// ... type info is discarded, only name is stored ...
// Line 280 — redundant re-stat for info we already had ✗
const stat = await fs.stat(dirSource);
isDirectory = stat.isDirectory();This means every skill incurs an extra filesystem round-trip. For N skills = 2N Recommended approach: use
|
| Current | Glob-based | |
|---|---|---|
stat calls per skill |
2 (readdir + re-stat) | 0 (type comes from glob match) |
| Parallel discovery | No | Yes (Promise.all) |
| Pattern flexibility | Manual filter | Declarative glob patterns |
| Lines of code | ~45 | ~25 |
The Promise.all for the two glob calls also parallelises discovery of both layouts, which is a nice bonus.
CI Report✅ All checks passed Pipeline Status
Test Results✅ 1581 passed Code CoverageCombined (Unit + Integration)
Coverage breakdown by test suiteUnit Tests
Integration Tests
Coverage thresholds are auto-ratcheted — they only go upVitest 📋 View full run · Generated by CI |
Switch skill source discovery to glob patterns for flat and directory layouts, removing redundant per-skill stat checks and simplifying install flow. Made-with: Cursor
|
Good idea. I have switched skill discovery to glob patterns and removed the redundant stat checks in setup flow. |
…t/GitNexus into fix/dynamic-skill-discovery
|
@claude can you please recommend them the most valuable integration test for this? |
|
Claude finished @magyargergo's task in 1m 22s —— View job Most valuable integration test for glob-based skill discovery
ContextThe PR fixes a real bug: the old hardcoded The current test suite has no test for the setup/install flow at all — Recommended test: "newly-added skill is auto-discovered and installed"This is the single test that would have caught the original bug and validates all three distinct behaviors of the new implementation in one scenario: // test/integration/skill-install.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
// ── test helper: call installSkillsTo via a thin re-export ──────────────────
// Add to setup.ts: export { installSkillsTo } (or test via the CLI setup command)
import { installSkillsTo } from '../../src/cli/setup.js';
describe('installSkillsTo – glob discovery', () => {
let skillsRoot: string;
let targetDir: string;
beforeEach(async () => {
// temp source that mimics the real skills/ directory
skillsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-skills-src-'));
targetDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-skills-dst-'));
});
afterEach(async () => {
await fs.rm(skillsRoot, { recursive: true, force: true });
await fs.rm(targetDir, { recursive: true, force: true });
});
it('installs a flat .md skill (legacy layout)', async () => {
await fs.writeFile(path.join(skillsRoot, 'gitnexus-guide.md'), '# Guide', 'utf-8');
const installed = await installSkillsTo(targetDir, skillsRoot);
expect(installed).toContain('gitnexus-guide');
const content = await fs.readFile(
path.join(targetDir, 'gitnexus-guide', 'SKILL.md'), 'utf-8'
);
expect(content).toBe('# Guide');
});
it('installs a directory-based skill recursively', async () => {
const srcDir = path.join(skillsRoot, 'gitnexus-pr-review');
await fs.mkdir(path.join(srcDir, 'references'), { recursive: true });
await fs.writeFile(path.join(srcDir, 'SKILL.md'), '# PR Review', 'utf-8');
await fs.writeFile(path.join(srcDir, 'references', 'extra.md'), '# Extra', 'utf-8');
const installed = await installSkillsTo(targetDir, skillsRoot);
expect(installed).toContain('gitnexus-pr-review');
expect(await fs.readFile(
path.join(targetDir, 'gitnexus-pr-review', 'SKILL.md'), 'utf-8'
)).toBe('# PR Review');
// sub-directory must be copied too
expect(await fs.readFile(
path.join(targetDir, 'gitnexus-pr-review', 'references', 'extra.md'), 'utf-8'
)).toBe('# Extra');
});
it('auto-discovers a newly added skill without code changes', async () => {
// seed known skills
await fs.writeFile(path.join(skillsRoot, 'gitnexus-guide.md'), '# Guide', 'utf-8');
// ← this is the skill that was silently missed before the fix
await fs.writeFile(path.join(skillsRoot, 'gitnexus-pr-review.md'), '# PR Review', 'utf-8');
const installed = await installSkillsTo(targetDir, skillsRoot);
// both must be installed; no manual array update required
expect(installed).toContain('gitnexus-guide');
expect(installed).toContain('gitnexus-pr-review');
expect(installed).toHaveLength(2);
});
it('directory skill takes precedence over flat .md with the same name', async () => {
// flat file
await fs.writeFile(path.join(skillsRoot, 'my-skill.md'), '# Flat', 'utf-8');
// directory version (richer layout)
const srcDir = path.join(skillsRoot, 'my-skill');
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(path.join(srcDir, 'SKILL.md'), '# Dir', 'utf-8');
const installed = await installSkillsTo(targetDir, skillsRoot);
// directory wins; only one copy installed
expect(installed.filter(n => n === 'my-skill')).toHaveLength(1);
expect(await fs.readFile(
path.join(targetDir, 'my-skill', 'SKILL.md'), 'utf-8'
)).toBe('# Dir');
});
});One required code change to enable this
// setup.ts (line 253)
async function installSkillsTo(targetDir: string, skillsRoot?: string): Promise<string[]> {
const root = skillsRoot ?? path.join(__dirname, '..', '..', 'skills');
// replace all `skillsRoot` references with `root`This is a one-line change that doesn't alter production behaviour at all. Why this test is the most valuable
|
Allow setup skill installation to accept an optional source root for testability and add an integration test that verifies flat and directory skill layouts, including directory precedence. Made-with: Cursor
Convert setup skill coverage to a black-box integration test using setupCommand and temporary HOME, and keep installSkillsTo internal with no test-only parameter. Made-with: Cursor
Validate setup skill installation for both flat-file and directory-based skill sources, including recursive directory copy behavior for nested files. Made-with: Cursor
|
Added basic integration coverage for setup skill installation to cursor root. |
Description
This PR fixes #269 by dynamically discovering the available skills in the
skills/directory instead of using a hardcodedSKILL_NAMESarray.Previously, if a new skill was added (such as
gitnexus-pr-review), it wouldn't be installed automatically duringgitnexus setupunless it was also manually added to the hardcoded array.Now,
installSkillsToscans the directory and automatically picks up both flat file (.md) and directory-based skills.Thank you @abhigyanpatwari for creating and maintaining this awesome project!