@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.
- package/README.md +185 -36
- package/dist/agents/ClaudeAgent.js +3 -0
- package/dist/agents/CodexCliAgent.js +3 -0
- package/dist/agents/CopilotAgent.js +3 -0
- package/dist/agents/CursorAgent.js +3 -0
- package/dist/cli/commands.js +4 -0
- package/dist/cli/handlers.js +9 -1
- package/dist/constants.js +7 -1
- package/dist/core/ConfigLoader.js +79 -1
- package/dist/core/FileSystemUtils.js +26 -4
- package/dist/core/SubagentsProcessor.js +440 -0
- package/dist/core/SubagentsUtils.js +195 -0
- package/dist/core/apply-engine.js +17 -14
- package/dist/lib.js +42 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
60
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
71
|
+
* Returns true when `.ruler/agents/*.md` should be concatenated into the
|
|
72
|
+
* generated top-level rule files. Defaults to false.
|
|
69
73
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|