@intellectronica/ruler 0.3.38 → 0.3.40

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.
@@ -0,0 +1,440 @@
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.discoverSubagents = discoverSubagents;
37
+ exports.getSelectedSubagentTargets = getSelectedSubagentTargets;
38
+ exports.getSubagentsGitignorePaths = getSubagentsGitignorePaths;
39
+ exports._resetExperimentalWarningForTests = _resetExperimentalWarningForTests;
40
+ exports.propagateSubagentsForClaude = propagateSubagentsForClaude;
41
+ exports.propagateSubagentsForCursor = propagateSubagentsForCursor;
42
+ exports.propagateSubagentsForCodex = propagateSubagentsForCodex;
43
+ exports.propagateSubagentsForCopilot = propagateSubagentsForCopilot;
44
+ exports.propagateSubagents = propagateSubagents;
45
+ const path = __importStar(require("path"));
46
+ const fs = __importStar(require("fs/promises"));
47
+ const yaml = __importStar(require("js-yaml"));
48
+ const toml_1 = require("@iarna/toml");
49
+ const constants_1 = require("../constants");
50
+ const SubagentsUtils_1 = require("./SubagentsUtils");
51
+ /**
52
+ * Discovers subagent definitions in `.ruler/agents/`.
53
+ * Each `.md` file is parsed for YAML frontmatter (name, description, …).
54
+ * Files that fail validation are dropped from the returned list and
55
+ * reported via warnings.
56
+ */
57
+ async function discoverSubagents(projectRoot) {
58
+ const dir = path.join(projectRoot, constants_1.RULER_SUBAGENTS_PATH);
59
+ try {
60
+ await fs.access(dir);
61
+ }
62
+ catch {
63
+ return { subagents: [], warnings: [] };
64
+ }
65
+ const entries = await fs.readdir(dir, { withFileTypes: true });
66
+ const mdFiles = entries
67
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
68
+ .map((entry) => path.join(dir, entry.name))
69
+ .sort();
70
+ const subagents = [];
71
+ const warnings = [];
72
+ for (const filePath of mdFiles) {
73
+ const info = await (0, SubagentsUtils_1.loadSubagentFile)(filePath);
74
+ if (info.valid) {
75
+ subagents.push(info);
76
+ }
77
+ else if (info.error) {
78
+ warnings.push(info.error);
79
+ }
80
+ }
81
+ return { subagents, warnings };
82
+ }
83
+ const SUBAGENT_TARGET_TO_IDENTIFIERS = new Map([
84
+ ['claude', ['claude']],
85
+ ['cursor', ['cursor']],
86
+ ['codex', ['codex']],
87
+ ['copilot', ['copilot']],
88
+ ]);
89
+ const SUBAGENT_TARGET_PATHS = {
90
+ claude: constants_1.CLAUDE_SUBAGENTS_PATH,
91
+ cursor: constants_1.CURSOR_SUBAGENTS_PATH,
92
+ codex: constants_1.CODEX_SUBAGENTS_PATH,
93
+ copilot: constants_1.COPILOT_SUBAGENTS_PATH,
94
+ };
95
+ /**
96
+ * Returns which native subagent targets are reachable through the supplied
97
+ * agent list. An agent only contributes to a target when it implements
98
+ * `supportsNativeSubagents()` returning true.
99
+ */
100
+ function getSelectedSubagentTargets(agents) {
101
+ const enabledIdentifiers = new Set(agents
102
+ .filter((agent) => agent.supportsNativeSubagents?.())
103
+ .map((agent) => agent.getIdentifier()));
104
+ const targets = new Set();
105
+ for (const [target, identifiers] of SUBAGENT_TARGET_TO_IDENTIFIERS) {
106
+ if (identifiers.some((id) => enabledIdentifiers.has(id))) {
107
+ targets.add(target);
108
+ }
109
+ }
110
+ return targets;
111
+ }
112
+ /**
113
+ * Returns absolute paths that subagent propagation may generate, for the
114
+ * supplied agents, used for `.gitignore` integration.
115
+ */
116
+ async function getSubagentsGitignorePaths(projectRoot, agents) {
117
+ const dir = path.join(projectRoot, constants_1.RULER_SUBAGENTS_PATH);
118
+ try {
119
+ await fs.access(dir);
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ const targets = getSelectedSubagentTargets(agents);
125
+ return Array.from(targets).map((t) => path.join(projectRoot, SUBAGENT_TARGET_PATHS[t]));
126
+ }
127
+ /**
128
+ * Module-level state to track if experimental warning has been shown.
129
+ * Mirrors the SkillsProcessor convention to avoid spamming the user across
130
+ * multiple `apply` invocations within the same process.
131
+ */
132
+ let hasWarnedExperimental = false;
133
+ function warnOnceExperimental(dryRun) {
134
+ if (hasWarnedExperimental)
135
+ return;
136
+ hasWarnedExperimental = true;
137
+ (0, constants_1.logWarn)('Subagents support is experimental and behavior may change in future releases.', dryRun);
138
+ }
139
+ /**
140
+ * Test-only hook to reset the once-per-process experimental warning state.
141
+ */
142
+ function _resetExperimentalWarningForTests() {
143
+ hasWarnedExperimental = false;
144
+ }
145
+ /* ------------------------------------------------------------------ */
146
+ /* Frontmatter helpers */
147
+ /* ------------------------------------------------------------------ */
148
+ function buildFrontmatterBlock(meta) {
149
+ const yamlText = yaml.dump(meta, { lineWidth: -1, noRefs: true }).trimEnd();
150
+ return `---\n${yamlText}\n---\n`;
151
+ }
152
+ function ensureBodyFormatting(body) {
153
+ const text = (body ?? '').replace(/^\n+/, '');
154
+ return text.endsWith('\n') ? text : `${text}\n`;
155
+ }
156
+ /* ------------------------------------------------------------------ */
157
+ /* Atomic directory write */
158
+ /* ------------------------------------------------------------------ */
159
+ /**
160
+ * Stages files into a temp directory and atomically swaps it into place.
161
+ * Mirrors the pattern used by SkillsProcessor for safe overwriting.
162
+ */
163
+ async function writeAgentsDirectoryAtomic(targetDir, files) {
164
+ const parent = path.dirname(targetDir);
165
+ await fs.mkdir(parent, { recursive: true });
166
+ const tempDir = path.join(parent, `agents.tmp-${Date.now()}`);
167
+ await fs.mkdir(tempDir, { recursive: true });
168
+ try {
169
+ for (const { name, content } of files) {
170
+ await fs.writeFile(path.join(tempDir, name), content, 'utf8');
171
+ }
172
+ try {
173
+ await fs.rm(targetDir, { recursive: true, force: true });
174
+ }
175
+ catch {
176
+ // Target didn't exist; ignore.
177
+ }
178
+ await fs.rename(tempDir, targetDir);
179
+ }
180
+ catch (error) {
181
+ try {
182
+ await fs.rm(tempDir, { recursive: true, force: true });
183
+ }
184
+ catch {
185
+ // Ignore cleanup errors.
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+ function buildClaudeFile(sub) {
191
+ const fm = sub.frontmatter;
192
+ const meta = {
193
+ name: fm.name,
194
+ description: fm.description,
195
+ };
196
+ if (fm.tools !== undefined)
197
+ meta.tools = fm.tools;
198
+ if (fm.model !== undefined)
199
+ meta.model = fm.model;
200
+ // Pass through readonly and is_background verbatim so authoring intent
201
+ // survives the Claude transform. Claude Code ignores unknown frontmatter
202
+ // keys, but downstream tooling that reads .claude/agents/*.md can still
203
+ // observe the original values.
204
+ if (fm.readonly !== undefined)
205
+ meta.readonly = fm.readonly;
206
+ if (fm.is_background !== undefined)
207
+ meta.is_background = fm.is_background;
208
+ return `${buildFrontmatterBlock(meta)}\n${ensureBodyFormatting(sub.body)}`;
209
+ }
210
+ function buildCursorFile(sub) {
211
+ const fm = sub.frontmatter;
212
+ const meta = {
213
+ name: fm.name,
214
+ description: fm.description,
215
+ model: fm.model ?? 'inherit',
216
+ readonly: fm.readonly ?? false,
217
+ is_background: fm.is_background ?? false,
218
+ };
219
+ return `${buildFrontmatterBlock(meta)}\n${ensureBodyFormatting(sub.body)}`;
220
+ }
221
+ function buildCodexFile(sub) {
222
+ const fm = sub.frontmatter;
223
+ const config = {
224
+ name: fm.name,
225
+ description: fm.description,
226
+ developer_instructions: ensureBodyFormatting(sub.body),
227
+ };
228
+ if (fm.model !== undefined && fm.model !== 'inherit') {
229
+ config.model = fm.model;
230
+ }
231
+ if (fm.readonly === true) {
232
+ config.sandbox_mode = 'read-only';
233
+ }
234
+ // @iarna/toml requires JsonMap; the cast is safe because every value is a
235
+ // string/boolean/number/object that the library knows how to serialize.
236
+ return (0, toml_1.stringify)(config);
237
+ }
238
+ function buildCopilotFile(sub, dryRun, verbose) {
239
+ const fm = sub.frontmatter;
240
+ const meta = {
241
+ name: fm.name,
242
+ description: fm.description,
243
+ 'user-invocable': true,
244
+ };
245
+ const warnings = [];
246
+ if (fm.tools && fm.tools.length > 0) {
247
+ const { tools, unknown } = (0, SubagentsUtils_1.mapToolsForCopilot)(fm.tools);
248
+ if (tools.length > 0) {
249
+ meta.tools = tools;
250
+ }
251
+ if (unknown.length > 0) {
252
+ warnings.push(`Subagent "${fm.name}": dropping tools not mappable to Copilot aliases: ${unknown.join(', ')}`);
253
+ }
254
+ }
255
+ if (fm.model !== undefined && fm.model !== 'inherit') {
256
+ meta.model = fm.model;
257
+ }
258
+ if (fm.readonly === true) {
259
+ meta['disable-model-invocation'] = true;
260
+ }
261
+ // Tool-drop is informational — surface it only when the user explicitly
262
+ // asked for detail (--verbose) or when previewing changes (--dry-run).
263
+ // A normal apply stays quiet to avoid noise on every run.
264
+ if (verbose || dryRun) {
265
+ for (const warning of warnings) {
266
+ (0, constants_1.logWarn)(warning, dryRun);
267
+ }
268
+ }
269
+ return {
270
+ content: `${buildFrontmatterBlock(meta)}\n${ensureBodyFormatting(sub.body)}`,
271
+ warnings,
272
+ };
273
+ }
274
+ async function propagateSubagentsForClaude(projectRoot, subagents, options) {
275
+ if (subagents.length === 0)
276
+ return [];
277
+ const targetDir = path.join(projectRoot, constants_1.CLAUDE_SUBAGENTS_PATH);
278
+ if (options.dryRun) {
279
+ return subagents.map((s) => `Write ${path.join(constants_1.CLAUDE_SUBAGENTS_PATH, `${s.name}.md`)}`);
280
+ }
281
+ const files = subagents.map((s) => ({
282
+ name: `${s.name}.md`,
283
+ content: buildClaudeFile(s),
284
+ }));
285
+ await writeAgentsDirectoryAtomic(targetDir, files);
286
+ return [];
287
+ }
288
+ async function propagateSubagentsForCursor(projectRoot, subagents, options) {
289
+ if (subagents.length === 0)
290
+ return [];
291
+ const targetDir = path.join(projectRoot, constants_1.CURSOR_SUBAGENTS_PATH);
292
+ if (options.dryRun) {
293
+ return subagents.map((s) => `Write ${path.join(constants_1.CURSOR_SUBAGENTS_PATH, `${s.name}.md`)}`);
294
+ }
295
+ const files = subagents.map((s) => ({
296
+ name: `${s.name}.md`,
297
+ content: buildCursorFile(s),
298
+ }));
299
+ await writeAgentsDirectoryAtomic(targetDir, files);
300
+ return [];
301
+ }
302
+ async function propagateSubagentsForCodex(projectRoot, subagents, options) {
303
+ if (subagents.length === 0)
304
+ return [];
305
+ const targetDir = path.join(projectRoot, constants_1.CODEX_SUBAGENTS_PATH);
306
+ if (options.dryRun) {
307
+ return subagents.map((s) => `Write ${path.join(constants_1.CODEX_SUBAGENTS_PATH, `${s.name}.toml`)}`);
308
+ }
309
+ const files = subagents.map((s) => ({
310
+ name: `${s.name}.toml`,
311
+ content: buildCodexFile(s),
312
+ }));
313
+ await writeAgentsDirectoryAtomic(targetDir, files);
314
+ return [];
315
+ }
316
+ async function propagateSubagentsForCopilot(projectRoot, subagents, options) {
317
+ if (subagents.length === 0)
318
+ return [];
319
+ const targetDir = path.join(projectRoot, constants_1.COPILOT_SUBAGENTS_PATH);
320
+ const verbose = options.verbose ?? false;
321
+ if (options.dryRun) {
322
+ const planLines = [];
323
+ for (const s of subagents) {
324
+ // Surface tool-mapping warnings during dry-run too — buildCopilotFile
325
+ // emits when dryRun is true so users previewing a change can see
326
+ // which tools would be dropped before it actually happens.
327
+ buildCopilotFile(s, true, verbose);
328
+ planLines.push(`Write ${path.join(constants_1.COPILOT_SUBAGENTS_PATH, `${s.name}.md`)}`);
329
+ }
330
+ return planLines;
331
+ }
332
+ const files = subagents.map((s) => ({
333
+ name: `${s.name}.md`,
334
+ content: buildCopilotFile(s, false, verbose).content,
335
+ }));
336
+ await writeAgentsDirectoryAtomic(targetDir, files);
337
+ return [];
338
+ }
339
+ /* ------------------------------------------------------------------ */
340
+ /* Cleanup-on-disable */
341
+ /* ------------------------------------------------------------------ */
342
+ async function cleanupSubagentsDir(projectRoot, relPath, dryRun, verbose) {
343
+ const target = path.join(projectRoot, relPath);
344
+ try {
345
+ await fs.access(target);
346
+ }
347
+ catch {
348
+ return;
349
+ }
350
+ if (dryRun) {
351
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${relPath}`, verbose, dryRun);
352
+ return;
353
+ }
354
+ await fs.rm(target, { recursive: true, force: true });
355
+ (0, constants_1.logVerboseInfo)(`Removed ${relPath} (subagents disabled)`, verbose, dryRun);
356
+ }
357
+ async function cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose) {
358
+ await cleanupSubagentsDir(projectRoot, constants_1.CLAUDE_SUBAGENTS_PATH, dryRun, verbose);
359
+ await cleanupSubagentsDir(projectRoot, constants_1.CURSOR_SUBAGENTS_PATH, dryRun, verbose);
360
+ await cleanupSubagentsDir(projectRoot, constants_1.CODEX_SUBAGENTS_PATH, dryRun, verbose);
361
+ await cleanupSubagentsDir(projectRoot, constants_1.COPILOT_SUBAGENTS_PATH, dryRun, verbose);
362
+ }
363
+ /* ------------------------------------------------------------------ */
364
+ /* Orchestrator */
365
+ /* ------------------------------------------------------------------ */
366
+ async function propagateSubagents(projectRoot, agents, subagentsEnabled, verbose, dryRun) {
367
+ if (!subagentsEnabled) {
368
+ (0, constants_1.logVerboseInfo)('Subagents support disabled, cleaning up subagent directories', verbose, dryRun);
369
+ await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
370
+ return;
371
+ }
372
+ const sourceDir = path.join(projectRoot, constants_1.RULER_SUBAGENTS_PATH);
373
+ try {
374
+ await fs.access(sourceDir);
375
+ }
376
+ catch {
377
+ (0, constants_1.logVerboseInfo)('No .ruler/agents directory found, cleaning up any stale managed subagent directories', verbose, dryRun);
378
+ await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
379
+ return;
380
+ }
381
+ const { subagents, warnings } = await discoverSubagents(projectRoot);
382
+ for (const w of warnings)
383
+ (0, constants_1.logWarn)(w, dryRun);
384
+ if (subagents.length === 0) {
385
+ (0, constants_1.logVerboseInfo)('No valid subagents found in .ruler/agents; cleaning up any stale managed subagent directories', verbose, dryRun);
386
+ await cleanupAllSubagentsDirectories(projectRoot, dryRun, verbose);
387
+ return;
388
+ }
389
+ (0, constants_1.logVerboseInfo)(`Discovered ${subagents.length} subagent(s)`, verbose, dryRun);
390
+ const supporting = agents.filter((a) => a.supportsNativeSubagents?.());
391
+ const nonSupporting = agents.filter((a) => !a.supportsNativeSubagents?.());
392
+ if (nonSupporting.length > 0) {
393
+ const names = nonSupporting.map((a) => a.getName()).join(', ');
394
+ (0, constants_1.logWarn)(`Subagents are configured, but the following agents do not support native subagents and will be skipped: ${names}`, dryRun);
395
+ }
396
+ const targets = getSelectedSubagentTargets(agents);
397
+ // Reconcile: any managed target directory that is not in the current
398
+ // selection set is stale and must be removed. This catches the case where
399
+ // a user drops an agent (e.g. claude+cursor → claude only) so the previously
400
+ // generated .cursor/agents/ directory does not linger as orphaned config.
401
+ const allTargets = ['claude', 'cursor', 'codex', 'copilot'];
402
+ for (const target of allTargets) {
403
+ if (!targets.has(target)) {
404
+ await cleanupSubagentsDir(projectRoot, SUBAGENT_TARGET_PATHS[target], dryRun, verbose);
405
+ }
406
+ }
407
+ if (supporting.length === 0) {
408
+ (0, constants_1.logVerboseInfo)('No agents support native subagents, skipping subagent propagation', verbose, dryRun);
409
+ return;
410
+ }
411
+ warnOnceExperimental(dryRun);
412
+ if (targets.has('claude')) {
413
+ (0, constants_1.logVerboseInfo)(`Writing subagents to ${constants_1.CLAUDE_SUBAGENTS_PATH} for Claude Code`, verbose, dryRun);
414
+ await propagateSubagentsForClaude(projectRoot, subagents, {
415
+ dryRun,
416
+ verbose,
417
+ });
418
+ }
419
+ if (targets.has('cursor')) {
420
+ (0, constants_1.logVerboseInfo)(`Writing subagents to ${constants_1.CURSOR_SUBAGENTS_PATH} for Cursor`, verbose, dryRun);
421
+ await propagateSubagentsForCursor(projectRoot, subagents, {
422
+ dryRun,
423
+ verbose,
424
+ });
425
+ }
426
+ if (targets.has('codex')) {
427
+ (0, constants_1.logVerboseInfo)(`Writing subagents to ${constants_1.CODEX_SUBAGENTS_PATH} for OpenAI Codex CLI`, verbose, dryRun);
428
+ await propagateSubagentsForCodex(projectRoot, subagents, {
429
+ dryRun,
430
+ verbose,
431
+ });
432
+ }
433
+ if (targets.has('copilot')) {
434
+ (0, constants_1.logVerboseInfo)(`Writing subagents to ${constants_1.COPILOT_SUBAGENTS_PATH} for GitHub Copilot`, verbose, dryRun);
435
+ await propagateSubagentsForCopilot(projectRoot, subagents, {
436
+ dryRun,
437
+ verbose,
438
+ });
439
+ }
440
+ }
@@ -0,0 +1,195 @@
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.parseFrontmatter = parseFrontmatter;
37
+ exports.validateFrontmatter = validateFrontmatter;
38
+ exports.loadSubagentFile = loadSubagentFile;
39
+ exports.mapToolsForCopilot = mapToolsForCopilot;
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs/promises"));
42
+ const yaml = __importStar(require("js-yaml"));
43
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
44
+ /**
45
+ * Extracts YAML frontmatter and body from a Markdown file's contents.
46
+ * Returns null if no frontmatter delimiter pair is present at the head of the file.
47
+ */
48
+ function parseFrontmatter(content) {
49
+ const match = FRONTMATTER_RE.exec(content);
50
+ if (!match) {
51
+ return null;
52
+ }
53
+ const [, raw, body] = match;
54
+ const meta = yaml.load(raw);
55
+ return {
56
+ meta: meta && typeof meta === 'object' ? meta : {},
57
+ body: body ?? '',
58
+ };
59
+ }
60
+ /**
61
+ * Validates a parsed frontmatter object and reads the required and optional
62
+ * fields into a typed SubagentFrontmatter. Returns the typed value on success
63
+ * or an error message on failure.
64
+ */
65
+ function validateFrontmatter(meta, expectedName) {
66
+ const name = meta.name;
67
+ const description = meta.description;
68
+ if (typeof name !== 'string' || name.length === 0) {
69
+ return { error: `missing or invalid required field "name"` };
70
+ }
71
+ if (typeof description !== 'string' || description.length === 0) {
72
+ return { error: `missing or invalid required field "description"` };
73
+ }
74
+ if (name !== expectedName) {
75
+ return {
76
+ error: `frontmatter name "${name}" does not match filename stem "${expectedName}"`,
77
+ };
78
+ }
79
+ const fm = { name, description };
80
+ if (meta.tools !== undefined) {
81
+ if (Array.isArray(meta.tools) &&
82
+ meta.tools.every((t) => typeof t === 'string')) {
83
+ fm.tools = meta.tools;
84
+ }
85
+ else if (typeof meta.tools === 'string') {
86
+ fm.tools = meta.tools
87
+ .split(',')
88
+ .map((t) => t.trim())
89
+ .filter(Boolean);
90
+ }
91
+ else {
92
+ return { error: `invalid "tools" field; expected string or string[]` };
93
+ }
94
+ }
95
+ if (meta.model !== undefined) {
96
+ if (typeof meta.model !== 'string') {
97
+ return { error: `invalid "model" field; expected string` };
98
+ }
99
+ fm.model = meta.model;
100
+ }
101
+ if (meta.readonly !== undefined) {
102
+ if (typeof meta.readonly !== 'boolean') {
103
+ return { error: `invalid "readonly" field; expected boolean` };
104
+ }
105
+ fm.readonly = meta.readonly;
106
+ }
107
+ if (meta.is_background !== undefined) {
108
+ if (typeof meta.is_background !== 'boolean') {
109
+ return { error: `invalid "is_background" field; expected boolean` };
110
+ }
111
+ fm.is_background = meta.is_background;
112
+ }
113
+ return { value: fm };
114
+ }
115
+ /**
116
+ * Loads a single subagent file and produces a SubagentInfo.
117
+ * Invalid files produce a SubagentInfo with valid=false and an error string.
118
+ */
119
+ async function loadSubagentFile(filePath) {
120
+ const stem = path.basename(filePath, '.md');
121
+ const content = await fs.readFile(filePath, 'utf8');
122
+ // js-yaml throws on malformed YAML; convert that into the standard
123
+ // validation-failure shape so one bad file doesn't abort discovery.
124
+ let parsed;
125
+ try {
126
+ parsed = parseFrontmatter(content);
127
+ }
128
+ catch (error) {
129
+ const detail = error instanceof Error ? error.message : String(error);
130
+ return {
131
+ name: stem,
132
+ path: filePath,
133
+ valid: false,
134
+ error: `${stem}.md: invalid YAML frontmatter: ${detail}`,
135
+ };
136
+ }
137
+ if (!parsed) {
138
+ return {
139
+ name: stem,
140
+ path: filePath,
141
+ valid: false,
142
+ error: `${stem}.md: missing YAML frontmatter`,
143
+ };
144
+ }
145
+ const result = validateFrontmatter(parsed.meta, stem);
146
+ if ('error' in result) {
147
+ return {
148
+ name: stem,
149
+ path: filePath,
150
+ valid: false,
151
+ error: `${stem}.md: ${result.error}`,
152
+ };
153
+ }
154
+ return {
155
+ name: stem,
156
+ path: filePath,
157
+ valid: true,
158
+ frontmatter: result.value,
159
+ body: parsed.body,
160
+ };
161
+ }
162
+ /**
163
+ * Maps Claude Code tool names to GitHub Copilot tool aliases.
164
+ * Unknown source tools return undefined and should be dropped (with a warning).
165
+ */
166
+ const COPILOT_TOOL_MAP = {
167
+ Read: 'read',
168
+ Grep: 'search',
169
+ Glob: 'search',
170
+ Bash: 'execute',
171
+ Edit: 'edit',
172
+ Write: 'edit',
173
+ WebFetch: 'web',
174
+ WebSearch: 'web',
175
+ TodoWrite: 'todo',
176
+ Task: 'agent',
177
+ };
178
+ /**
179
+ * Translates Claude tool names to Copilot aliases. Deduplicates results.
180
+ * Unknown source tools are reported separately so callers can surface a warning.
181
+ */
182
+ function mapToolsForCopilot(sourceTools) {
183
+ const mapped = new Set();
184
+ const unknown = [];
185
+ for (const tool of sourceTools) {
186
+ const alias = COPILOT_TOOL_MAP[tool];
187
+ if (alias) {
188
+ mapped.add(alias);
189
+ }
190
+ else {
191
+ unknown.push(tool);
192
+ }
193
+ }
194
+ return { tools: Array.from(mapped), unknown };
195
+ }
@@ -56,25 +56,23 @@ const constants_1 = require("../constants");
56
56
  async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
57
57
  const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
58
58
  const results = [];
59
- const rulerDirConfigs = await processIndependentRulerDirs(rulerDirs);
60
- for (const { rulerDir, files } of rulerDirConfigs) {
59
+ // Load config first so we know whether `.ruler/agents/` should be included
60
+ // in the rule concatenation for each directory.
61
+ for (const rulerDir of rulerDirs) {
61
62
  const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
63
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDir, {
64
+ includeAgents: shouldIncludeAgentsInRules(config),
65
+ });
62
66
  results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
63
67
  }
64
68
  return results;
65
69
  }
66
70
  /**
67
- * Processes each .ruler directory independently, returning configuration for each.
68
- * Each .ruler directory gets its own rules (not merged with others).
71
+ * Returns true when `.ruler/agents/*.md` should be concatenated into the
72
+ * generated top-level rule files. Defaults to false.
69
73
  */
70
- async function processIndependentRulerDirs(rulerDirs) {
71
- const results = [];
72
- // Process each .ruler directory independently
73
- for (const rulerDir of rulerDirs) {
74
- const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
75
- results.push({ rulerDir, files });
76
- }
77
- return results;
74
+ function shouldIncludeAgentsInRules(config) {
75
+ return config.subagents?.include_in_rules === true;
78
76
  }
79
77
  async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
80
78
  await warnAboutLegacyMcpJson(rulerDir);
@@ -146,6 +144,8 @@ function cloneLoadedConfig(config) {
146
144
  cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
147
145
  mcp: config.mcp ? { ...config.mcp } : undefined,
148
146
  gitignore: config.gitignore ? { ...config.gitignore } : undefined,
147
+ skills: config.skills ? { ...config.skills } : undefined,
148
+ subagents: config.subagents ? { ...config.subagents } : undefined,
149
149
  nested: config.nested,
150
150
  nestedDefined: config.nestedDefined,
151
151
  };
@@ -203,8 +203,11 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
203
203
  projectRoot,
204
204
  configPath,
205
205
  });
206
- // Read rule files
207
- const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0]);
206
+ // Read rule files. `.ruler/agents/` is only included when
207
+ // `[agents] include_in_rules = true`.
208
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0], {
209
+ includeAgents: shouldIncludeAgentsInRules(config),
210
+ });
208
211
  // Concatenate rules
209
212
  const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(primaryDir));
210
213
  // Load unified config to get merged MCP configuration