@intellectronica/ruler 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,8 +69,34 @@ const rulerConfigSchema = zod_1.z.object({
69
69
  enabled: zod_1.z.boolean().optional(),
70
70
  })
71
71
  .optional(),
72
+ skills: zod_1.z
73
+ .object({
74
+ enabled: zod_1.z.boolean().optional(),
75
+ })
76
+ .optional(),
72
77
  nested: zod_1.z.boolean().optional(),
73
78
  });
79
+ /**
80
+ * Recursively creates a new object with only enumerable string keys,
81
+ * effectively excluding Symbol properties.
82
+ * The @iarna/toml parser adds Symbol properties (Symbol(type), Symbol(declared))
83
+ * for metadata, which Zod v4+ validates and rejects as invalid record keys.
84
+ * By rebuilding the object structure using Object.keys(), we create clean objects
85
+ * that only contain the actual data without Symbol metadata.
86
+ */
87
+ function stripSymbols(obj) {
88
+ if (obj === null || typeof obj !== 'object') {
89
+ return obj;
90
+ }
91
+ if (Array.isArray(obj)) {
92
+ return obj.map(stripSymbols);
93
+ }
94
+ const result = {};
95
+ for (const key of Object.keys(obj)) {
96
+ result[key] = stripSymbols(obj[key]);
97
+ }
98
+ return result;
99
+ }
74
100
  /**
75
101
  * Loads and parses the ruler TOML configuration file, applying defaults.
76
102
  * If the file is missing or invalid, returns empty/default config.
@@ -97,7 +123,9 @@ async function loadConfig(options) {
97
123
  let raw = {};
98
124
  try {
99
125
  const text = await fs_1.promises.readFile(configFile, 'utf8');
100
- raw = text.trim() ? (0, toml_1.parse)(text) : {};
126
+ const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
127
+ // Strip Symbol properties added by @iarna/toml (required for Zod v4+)
128
+ raw = stripSymbols(parsed);
101
129
  // Validate the configuration with zod
102
130
  const validationResult = rulerConfigSchema.safeParse(raw);
103
131
  if (!validationResult.success) {
@@ -175,13 +203,23 @@ async function loadConfig(options) {
175
203
  if (typeof rawGitignoreSection.enabled === 'boolean') {
176
204
  gitignoreConfig.enabled = rawGitignoreSection.enabled;
177
205
  }
178
- const nested = typeof raw.nested === 'boolean' ? raw.nested : false;
206
+ const rawSkillsSection = raw.skills && typeof raw.skills === 'object' && !Array.isArray(raw.skills)
207
+ ? raw.skills
208
+ : {};
209
+ const skillsConfig = {};
210
+ if (typeof rawSkillsSection.enabled === 'boolean') {
211
+ skillsConfig.enabled = rawSkillsSection.enabled;
212
+ }
213
+ const nestedDefined = typeof raw.nested === 'boolean';
214
+ const nested = nestedDefined ? raw.nested : false;
179
215
  return {
180
216
  defaultAgents,
181
217
  agentConfigs,
182
218
  cliAgents,
183
219
  mcp: globalMcpConfig,
184
220
  gitignore: gitignoreConfig,
221
+ skills: skillsConfig,
185
222
  nested,
223
+ nestedDefined,
186
224
  };
187
225
  }
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.discoverSkills = discoverSkills;
37
+ exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
38
+ exports.propagateSkills = propagateSkills;
39
+ exports.propagateSkillsForClaude = propagateSkillsForClaude;
40
+ exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
41
+ exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
42
+ const path = __importStar(require("path"));
43
+ const fs = __importStar(require("fs/promises"));
44
+ const constants_1 = require("../constants");
45
+ const SkillsUtils_1 = require("./SkillsUtils");
46
+ /**
47
+ * Discovers skills in the project's .ruler/skills directory.
48
+ * Returns discovered skills and any validation warnings.
49
+ */
50
+ async function discoverSkills(projectRoot) {
51
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
52
+ // Check if skills directory exists
53
+ try {
54
+ await fs.access(skillsDir);
55
+ }
56
+ catch {
57
+ // Skills directory doesn't exist - this is fine, just return empty
58
+ return { skills: [], warnings: [] };
59
+ }
60
+ // Walk the skills tree
61
+ return await (0, SkillsUtils_1.walkSkillsTree)(skillsDir);
62
+ }
63
+ /**
64
+ * Gets the paths that skills will generate, for gitignore purposes.
65
+ * Returns empty array if skills directory doesn't exist.
66
+ */
67
+ async function getSkillsGitignorePaths(projectRoot) {
68
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
69
+ // Check if skills directory exists
70
+ try {
71
+ await fs.access(skillsDir);
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ // Import here to avoid circular dependency
77
+ const { CLAUDE_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
78
+ return [
79
+ path.join(projectRoot, CLAUDE_SKILLS_PATH),
80
+ path.join(projectRoot, SKILLZ_DIR),
81
+ ];
82
+ }
83
+ /**
84
+ * Module-level state to track if experimental warning has been shown.
85
+ * This ensures the warning appears once per process (CLI invocation), not once per apply call.
86
+ * This is intentional: warnings about experimental features should not spam the user
87
+ * if they run multiple applies in the same process or test suite.
88
+ */
89
+ let hasWarnedExperimental = false;
90
+ /**
91
+ * Warns once per process about experimental skills features and uv requirement.
92
+ * Uses module-level state to prevent duplicate warnings within the same process.
93
+ */
94
+ function warnOnceExperimentalAndUv(verbose, dryRun) {
95
+ if (hasWarnedExperimental) {
96
+ return;
97
+ }
98
+ hasWarnedExperimental = true;
99
+ (0, constants_1.logWarn)('Skills support is experimental and behavior may change in future releases.', dryRun);
100
+ (0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
101
+ }
102
+ /**
103
+ * Cleans up skills directories (.claude/skills and .skillz) when skills are disabled.
104
+ * This ensures that stale skills from previous runs don't persist when skills are turned off.
105
+ */
106
+ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
107
+ const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
108
+ const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
109
+ // Clean up .claude/skills
110
+ try {
111
+ await fs.access(claudeSkillsPath);
112
+ if (dryRun) {
113
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun);
114
+ }
115
+ else {
116
+ await fs.rm(claudeSkillsPath, { recursive: true, force: true });
117
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
118
+ }
119
+ }
120
+ catch {
121
+ // Directory doesn't exist, nothing to clean
122
+ }
123
+ // Clean up .skillz
124
+ try {
125
+ await fs.access(skillzPath);
126
+ if (dryRun) {
127
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.SKILLZ_DIR}`, verbose, dryRun);
128
+ }
129
+ else {
130
+ await fs.rm(skillzPath, { recursive: true, force: true });
131
+ (0, constants_1.logVerboseInfo)(`Removed ${constants_1.SKILLZ_DIR} (skills disabled)`, verbose, dryRun);
132
+ }
133
+ }
134
+ catch {
135
+ // Directory doesn't exist, nothing to clean
136
+ }
137
+ }
138
+ /**
139
+ * Propagates skills for agents that need them.
140
+ */
141
+ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun) {
142
+ if (!skillsEnabled) {
143
+ (0, constants_1.logVerboseInfo)('Skills support disabled, cleaning up skills directories', verbose, dryRun);
144
+ // Clean up skills directories when skills are disabled
145
+ await cleanupSkillsDirectories(projectRoot, dryRun, verbose);
146
+ return;
147
+ }
148
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
149
+ // Check if skills directory exists
150
+ try {
151
+ await fs.access(skillsDir);
152
+ }
153
+ catch {
154
+ // No skills directory - this is fine
155
+ (0, constants_1.logVerboseInfo)('No .ruler/skills directory found, skipping skills propagation', verbose, dryRun);
156
+ return;
157
+ }
158
+ // Discover skills
159
+ const { skills, warnings } = await discoverSkills(projectRoot);
160
+ if (warnings.length > 0) {
161
+ warnings.forEach((warning) => (0, constants_1.logWarn)(warning, dryRun));
162
+ }
163
+ if (skills.length === 0) {
164
+ (0, constants_1.logVerboseInfo)('No valid skills found in .ruler/skills', verbose, dryRun);
165
+ return;
166
+ }
167
+ (0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun);
168
+ // Check if any agents need skills
169
+ const hasNativeSkillsAgent = agents.some((a) => a.supportsNativeSkills?.());
170
+ const hasMcpAgent = agents.some((a) => a.supportsMcpStdio?.() && !a.supportsNativeSkills?.());
171
+ if (!hasNativeSkillsAgent && !hasMcpAgent) {
172
+ (0, constants_1.logVerboseInfo)('No agents require skills support', verbose, dryRun);
173
+ return;
174
+ }
175
+ // Warn about experimental features
176
+ if (hasMcpAgent) {
177
+ warnOnceExperimentalAndUv(verbose, dryRun);
178
+ }
179
+ // Copy to Claude skills directory if needed
180
+ if (hasNativeSkillsAgent) {
181
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code`, verbose, dryRun);
182
+ await propagateSkillsForClaude(projectRoot, { dryRun });
183
+ }
184
+ // Copy to .skillz directory if needed
185
+ if (hasMcpAgent) {
186
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.SKILLZ_DIR} for MCP agents`, verbose, dryRun);
187
+ await propagateSkillsForSkillz(projectRoot, { dryRun });
188
+ }
189
+ }
190
+ /**
191
+ * Propagates skills for Claude Code by copying .ruler/skills to .claude/skills.
192
+ * Uses atomic replace to ensure safe overwriting of existing skills.
193
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
194
+ */
195
+ async function propagateSkillsForClaude(projectRoot, options) {
196
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
197
+ const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
198
+ const claudeDir = path.dirname(claudeSkillsPath);
199
+ // Check if source skills directory exists
200
+ try {
201
+ await fs.access(skillsDir);
202
+ }
203
+ catch {
204
+ // No skills directory - return empty
205
+ return [];
206
+ }
207
+ if (options.dryRun) {
208
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CLAUDE_SKILLS_PATH}`];
209
+ }
210
+ // Ensure .claude directory exists
211
+ await fs.mkdir(claudeDir, { recursive: true });
212
+ // Use atomic replace: copy to temp, then rename
213
+ const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`);
214
+ try {
215
+ // Copy to temp directory
216
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
217
+ // Atomically replace the target
218
+ // First, remove existing target if it exists
219
+ try {
220
+ await fs.rm(claudeSkillsPath, { recursive: true, force: true });
221
+ }
222
+ catch {
223
+ // Target didn't exist, that's fine
224
+ }
225
+ // Rename temp to target
226
+ await fs.rename(tempDir, claudeSkillsPath);
227
+ }
228
+ catch (error) {
229
+ // Clean up temp directory on error
230
+ try {
231
+ await fs.rm(tempDir, { recursive: true, force: true });
232
+ }
233
+ catch {
234
+ // Ignore cleanup errors
235
+ }
236
+ throw error;
237
+ }
238
+ return [];
239
+ }
240
+ /**
241
+ * Propagates skills for MCP agents by copying .ruler/skills to .skillz.
242
+ * Uses atomic replace to ensure safe overwriting of existing skills.
243
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
244
+ */
245
+ async function propagateSkillsForSkillz(projectRoot, options) {
246
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
247
+ const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
248
+ // Check if source skills directory exists
249
+ try {
250
+ await fs.access(skillsDir);
251
+ }
252
+ catch {
253
+ // No skills directory - return empty
254
+ return [];
255
+ }
256
+ if (options.dryRun) {
257
+ return [
258
+ `Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.SKILLZ_DIR}`,
259
+ `Configure Skillz MCP server with absolute path to ${constants_1.SKILLZ_DIR}`,
260
+ ];
261
+ }
262
+ // Use atomic replace: copy to temp, then rename
263
+ const tempDir = path.join(projectRoot, `${constants_1.SKILLZ_DIR}.tmp-${Date.now()}`);
264
+ try {
265
+ // Copy to temp directory
266
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
267
+ // Atomically replace the target
268
+ // First, remove existing target if it exists
269
+ try {
270
+ await fs.rm(skillzPath, { recursive: true, force: true });
271
+ }
272
+ catch {
273
+ // Target didn't exist, that's fine
274
+ }
275
+ // Rename temp to target
276
+ await fs.rename(tempDir, skillzPath);
277
+ }
278
+ catch (error) {
279
+ // Clean up temp directory on error
280
+ try {
281
+ await fs.rm(tempDir, { recursive: true, force: true });
282
+ }
283
+ catch {
284
+ // Ignore cleanup errors
285
+ }
286
+ throw error;
287
+ }
288
+ return [];
289
+ }
290
+ /**
291
+ * Builds MCP config for Skillz server.
292
+ */
293
+ function buildSkillzMcpConfig(projectRoot) {
294
+ const skillzAbsPath = path.resolve(projectRoot, constants_1.SKILLZ_DIR);
295
+ return {
296
+ [constants_1.SKILLZ_MCP_SERVER_NAME]: {
297
+ command: 'uvx',
298
+ args: ['skillz@latest', skillzAbsPath],
299
+ },
300
+ };
301
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hasSkillMd = hasSkillMd;
37
+ exports.isGroupingDir = isGroupingDir;
38
+ exports.walkSkillsTree = walkSkillsTree;
39
+ exports.formatValidationWarnings = formatValidationWarnings;
40
+ exports.copySkillsDirectory = copySkillsDirectory;
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs/promises"));
43
+ const constants_1 = require("../constants");
44
+ /**
45
+ * Checks if a directory contains a SKILL.md file.
46
+ */
47
+ async function hasSkillMd(dirPath) {
48
+ try {
49
+ const skillMdPath = path.join(dirPath, constants_1.SKILL_MD_FILENAME);
50
+ await fs.access(skillMdPath);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
59
+ */
60
+ async function isGroupingDir(dirPath) {
61
+ try {
62
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
63
+ const subdirs = entries.filter((e) => e.isDirectory());
64
+ for (const subdir of subdirs) {
65
+ const subdirPath = path.join(dirPath, subdir.name);
66
+ if (await hasSkillMd(subdirPath)) {
67
+ return true;
68
+ }
69
+ // Check recursively for nested grouping
70
+ if (await isGroupingDir(subdirPath)) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Walks the skills tree and discovers all skills.
82
+ * Returns skills and any validation warnings.
83
+ */
84
+ async function walkSkillsTree(root) {
85
+ const skills = [];
86
+ const warnings = [];
87
+ async function walk(currentPath, relativePath) {
88
+ try {
89
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
90
+ for (const entry of entries) {
91
+ if (!entry.isDirectory()) {
92
+ continue;
93
+ }
94
+ const entryPath = path.join(currentPath, entry.name);
95
+ const entryRelativePath = relativePath
96
+ ? path.join(relativePath, entry.name)
97
+ : entry.name;
98
+ const hasSkill = await hasSkillMd(entryPath);
99
+ const isGrouping = !hasSkill && (await isGroupingDir(entryPath));
100
+ if (hasSkill) {
101
+ // This is a valid skill directory
102
+ skills.push({
103
+ name: entry.name,
104
+ path: entryPath,
105
+ hasSkillMd: true,
106
+ valid: true,
107
+ });
108
+ }
109
+ else if (isGrouping) {
110
+ // This is a grouping directory, recurse into it
111
+ await walk(entryPath, entryRelativePath);
112
+ }
113
+ else {
114
+ // This is neither a skill nor a grouping directory - warn about it
115
+ warnings.push(`Directory '${entryRelativePath}' in .ruler/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
116
+ }
117
+ }
118
+ }
119
+ catch (err) {
120
+ // If we can't read the directory, just return what we have
121
+ warnings.push(`Failed to read directory ${relativePath || 'root'}: ${err.message}`);
122
+ }
123
+ }
124
+ await walk(root, '');
125
+ return { skills, warnings };
126
+ }
127
+ /**
128
+ * Formats validation warnings for display.
129
+ */
130
+ function formatValidationWarnings(warnings) {
131
+ if (warnings.length === 0) {
132
+ return '';
133
+ }
134
+ return warnings.map((w) => ` - ${w}`).join('\n');
135
+ }
136
+ /**
137
+ * Recursively copies a directory and all its contents.
138
+ */
139
+ async function copyRecursive(src, dest) {
140
+ const stat = await fs.stat(src);
141
+ if (stat.isDirectory()) {
142
+ await fs.mkdir(dest, { recursive: true });
143
+ const entries = await fs.readdir(src, { withFileTypes: true });
144
+ for (const entry of entries) {
145
+ const srcPath = path.join(src, entry.name);
146
+ const destPath = path.join(dest, entry.name);
147
+ await copyRecursive(srcPath, destPath);
148
+ }
149
+ }
150
+ else {
151
+ await fs.copyFile(src, dest);
152
+ }
153
+ }
154
+ /**
155
+ * Copies the skills directory to the destination, preserving structure.
156
+ * Creates the destination directory if it doesn't exist.
157
+ */
158
+ async function copySkillsDirectory(srcDir, destDir) {
159
+ await fs.mkdir(destDir, { recursive: true });
160
+ await copyRecursive(srcDir, destDir);
161
+ }
@@ -85,12 +85,24 @@ async function loadUnifiedConfig(options) {
85
85
  typeof tomlRaw.nested === 'boolean') {
86
86
  nested = tomlRaw.nested;
87
87
  }
88
+ // Parse skills configuration
89
+ let skillsConfig;
90
+ if (tomlRaw && typeof tomlRaw === 'object') {
91
+ const skillsSection = tomlRaw.skills;
92
+ if (skillsSection && typeof skillsSection === 'object') {
93
+ const skillsObj = skillsSection;
94
+ if (typeof skillsObj.enabled === 'boolean') {
95
+ skillsConfig = { enabled: skillsObj.enabled };
96
+ }
97
+ }
98
+ }
88
99
  const toml = {
89
100
  raw: tomlRaw,
90
101
  schemaVersion: 1,
91
102
  agents: {},
92
103
  defaultAgents,
93
104
  nested,
105
+ skills: skillsConfig,
94
106
  };
95
107
  // Collect rule markdown files
96
108
  let ruleFiles = [];