@intellectronica/ruler 0.3.41 → 0.3.43

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.
Files changed (105) hide show
  1. package/README.md +135 -36
  2. package/dist/agents/AbstractAgent.d.ts +53 -0
  3. package/dist/agents/AbstractAgent.js +3 -2
  4. package/dist/agents/AgentsMdAgent.d.ts +14 -0
  5. package/dist/agents/AgentsMdAgent.js +3 -2
  6. package/dist/agents/AiderAgent.d.ts +14 -0
  7. package/dist/agents/AiderAgent.js +7 -4
  8. package/dist/agents/AmazonQCliAgent.d.ts +13 -0
  9. package/dist/agents/AmazonQCliAgent.js +6 -4
  10. package/dist/agents/AmpAgent.d.ts +6 -0
  11. package/dist/agents/AntigravityAgent.d.ts +10 -0
  12. package/dist/agents/AugmentCodeAgent.d.ts +13 -0
  13. package/dist/agents/AugmentCodeAgent.js +3 -2
  14. package/dist/agents/ClaudeAgent.d.ts +13 -0
  15. package/dist/agents/ClineAgent.d.ts +9 -0
  16. package/dist/agents/CodexCliAgent.d.ts +31 -0
  17. package/dist/agents/CodexCliAgent.js +1 -1
  18. package/dist/agents/CopilotAgent.d.ts +20 -0
  19. package/dist/agents/CrushAgent.d.ts +14 -0
  20. package/dist/agents/CrushAgent.js +18 -6
  21. package/dist/agents/CursorAgent.d.ts +17 -0
  22. package/dist/agents/FactoryDroidAgent.d.ts +13 -0
  23. package/dist/agents/FirebaseAgent.d.ts +11 -0
  24. package/dist/agents/FirebenderAgent.d.ts +36 -0
  25. package/dist/agents/FirebenderAgent.js +5 -4
  26. package/dist/agents/GeminiCliAgent.d.ts +12 -0
  27. package/dist/agents/GeminiCliAgent.js +13 -7
  28. package/dist/agents/GooseAgent.d.ts +12 -0
  29. package/dist/agents/IAgent.d.ts +74 -0
  30. package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
  31. package/dist/agents/JulesAgent.d.ts +5 -0
  32. package/dist/agents/JunieAgent.d.ts +12 -0
  33. package/dist/agents/KiloCodeAgent.d.ts +14 -0
  34. package/dist/agents/KiroAgent.d.ts +8 -0
  35. package/dist/agents/MistralVibeAgent.d.ts +31 -0
  36. package/dist/agents/MistralVibeAgent.js +14 -3
  37. package/dist/agents/OpenCodeAgent.d.ts +11 -0
  38. package/dist/agents/OpenCodeAgent.js +24 -12
  39. package/dist/agents/OpenHandsAgent.d.ts +8 -0
  40. package/dist/agents/PiAgent.d.ts +9 -0
  41. package/dist/agents/QwenCodeAgent.d.ts +11 -0
  42. package/dist/agents/QwenCodeAgent.js +11 -5
  43. package/dist/agents/RooCodeAgent.d.ts +16 -0
  44. package/dist/agents/RooCodeAgent.js +3 -2
  45. package/dist/agents/TraeAgent.d.ts +10 -0
  46. package/dist/agents/WarpAgent.d.ts +12 -0
  47. package/dist/agents/WindsurfAgent.d.ts +13 -0
  48. package/dist/agents/ZedAgent.d.ts +21 -0
  49. package/dist/agents/ZedAgent.js +8 -5
  50. package/dist/agents/agent-utils.d.ts +5 -0
  51. package/dist/agents/agent-utils.js +8 -5
  52. package/dist/agents/index.d.ts +9 -0
  53. package/dist/cli/commands.d.ts +4 -0
  54. package/dist/cli/commands.js +1 -2
  55. package/dist/cli/handlers.d.ts +41 -0
  56. package/dist/cli/handlers.js +75 -59
  57. package/dist/cli/index.d.ts +2 -0
  58. package/dist/constants.d.ts +35 -0
  59. package/dist/constants.js +1 -1
  60. package/dist/core/ConfigLoader.d.ts +59 -0
  61. package/dist/core/ConfigLoader.js +178 -44
  62. package/dist/core/FileSystemUtils.d.ts +53 -0
  63. package/dist/core/FileSystemUtils.js +157 -20
  64. package/dist/core/GitignoreUtils.d.ts +25 -0
  65. package/dist/core/GitignoreUtils.js +94 -32
  66. package/dist/core/RuleProcessor.d.ts +8 -0
  67. package/dist/core/SkillsProcessor.d.ts +127 -0
  68. package/dist/core/SkillsProcessor.js +118 -223
  69. package/dist/core/SkillsUtils.d.ts +26 -0
  70. package/dist/core/SubagentsProcessor.d.ts +38 -0
  71. package/dist/core/SubagentsProcessor.js +8 -5
  72. package/dist/core/SubagentsUtils.d.ts +34 -0
  73. package/dist/core/UnifiedConfigLoader.d.ts +10 -0
  74. package/dist/core/UnifiedConfigLoader.js +115 -33
  75. package/dist/core/UnifiedConfigTypes.d.ts +97 -0
  76. package/dist/core/agent-selection.d.ts +12 -0
  77. package/dist/core/agent-selection.js +17 -7
  78. package/dist/core/apply-engine.d.ts +70 -0
  79. package/dist/core/apply-engine.js +88 -58
  80. package/dist/core/config-utils.d.ts +14 -0
  81. package/dist/core/config-utils.js +9 -3
  82. package/dist/core/hash.d.ts +2 -0
  83. package/dist/core/path-utils.d.ts +1 -0
  84. package/dist/core/path-utils.js +42 -0
  85. package/dist/core/revert-engine.d.ts +37 -0
  86. package/dist/core/revert-engine.js +142 -34
  87. package/dist/lib.d.ts +13 -0
  88. package/dist/lib.js +24 -8
  89. package/dist/mcp/capabilities.d.ts +20 -0
  90. package/dist/mcp/merge.d.ts +10 -0
  91. package/dist/mcp/merge.js +36 -16
  92. package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
  93. package/dist/mcp/propagateOpenCodeMcp.js +30 -11
  94. package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
  95. package/dist/mcp/propagateOpenHandsMcp.js +48 -21
  96. package/dist/mcp/validate.d.ts +7 -0
  97. package/dist/mcp/validate.js +6 -1
  98. package/dist/paths/mcp.d.ts +8 -0
  99. package/dist/paths/mcp.js +44 -8
  100. package/dist/revert.d.ts +6 -0
  101. package/dist/revert.js +58 -46
  102. package/dist/types.d.ts +87 -0
  103. package/dist/vscode/settings.d.ts +40 -0
  104. package/dist/vscode/settings.js +3 -3
  105. package/package.json +8 -5
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.discoverSkills = discoverSkills;
37
37
  exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
38
+ exports.replaceSkillsDirectory = replaceSkillsDirectory;
38
39
  exports.propagateSkills = propagateSkills;
39
40
  exports.propagateSkillsForClaude = propagateSkillsForClaude;
40
41
  exports.propagateSkillsForCodex = propagateSkillsForCodex;
@@ -53,6 +54,7 @@ const path = __importStar(require("path"));
53
54
  const fs = __importStar(require("fs/promises"));
54
55
  const constants_1 = require("../constants");
55
56
  const SkillsUtils_1 = require("./SkillsUtils");
57
+ const FileSystemUtils_1 = require("./FileSystemUtils");
56
58
  /**
57
59
  * Discovers skills in the project's .ruler/skills directory.
58
60
  * Returns discovered skills and any validation warnings.
@@ -101,7 +103,8 @@ async function getSkillsGitignorePaths(projectRoot, agents) {
101
103
  factory: FACTORY_SKILLS_PATH,
102
104
  antigravity: ANTIGRAVITY_SKILLS_PATH,
103
105
  };
104
- return Array.from(selectedTargets).map((target) => path.join(projectRoot, targetPaths[target]));
106
+ const pathSet = new Set(Array.from(selectedTargets).map((target) => path.join(projectRoot, targetPaths[target])));
107
+ return Array.from(pathSet);
105
108
  }
106
109
  /**
107
110
  * Module-level state to track if experimental warning has been shown.
@@ -126,7 +129,7 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
126
129
  ['codex', ['codex']],
127
130
  ['opencode', ['opencode']],
128
131
  ['pi', ['pi']],
129
- ['goose', ['goose', 'amp']],
132
+ ['goose', ['goose', 'amp', 'zed']],
130
133
  ['vibe', ['mistral']],
131
134
  ['roo', ['roo']],
132
135
  ['gemini', ['gemini-cli']],
@@ -136,6 +139,21 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
136
139
  ['factory', ['factory']],
137
140
  ['antigravity', ['antigravity']],
138
141
  ]);
142
+ const SKILL_TARGET_PATHS = Array.from(new Set([
143
+ constants_1.CLAUDE_SKILLS_PATH,
144
+ constants_1.CODEX_SKILLS_PATH,
145
+ constants_1.OPENCODE_SKILLS_PATH,
146
+ constants_1.PI_SKILLS_PATH,
147
+ constants_1.GOOSE_SKILLS_PATH,
148
+ constants_1.VIBE_SKILLS_PATH,
149
+ constants_1.ROO_SKILLS_PATH,
150
+ constants_1.GEMINI_SKILLS_PATH,
151
+ constants_1.JUNIE_SKILLS_PATH,
152
+ constants_1.CURSOR_SKILLS_PATH,
153
+ constants_1.WINDSURF_SKILLS_PATH,
154
+ constants_1.FACTORY_SKILLS_PATH,
155
+ constants_1.ANTIGRAVITY_SKILLS_PATH,
156
+ ]));
139
157
  function getSelectedSkillTargets(agents) {
140
158
  const selectedIdentifiers = new Set(agents
141
159
  .filter((agent) => agent.supportsNativeSkills?.())
@@ -148,205 +166,82 @@ function getSelectedSkillTargets(agents) {
148
166
  }
149
167
  return targets;
150
168
  }
151
- /**
152
- * Cleans up skills directories when skills are disabled.
153
- * This ensures that stale skills from previous runs don't persist when skills are turned off.
154
- */
155
- async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
156
- const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
157
- const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
158
- const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
159
- const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH);
160
- const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
161
- const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
162
- const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH);
163
- const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH);
164
- const junieSkillsPath = path.join(projectRoot, constants_1.JUNIE_SKILLS_PATH);
165
- const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH);
166
- const windsurfSkillsPath = path.join(projectRoot, constants_1.WINDSURF_SKILLS_PATH);
167
- const factorySkillsPath = path.join(projectRoot, constants_1.FACTORY_SKILLS_PATH);
168
- const antigravitySkillsPath = path.join(projectRoot, constants_1.ANTIGRAVITY_SKILLS_PATH);
169
- // Clean up .claude/skills
170
- try {
171
- await fs.access(claudeSkillsPath);
172
- if (dryRun) {
173
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun);
174
- }
175
- else {
176
- await fs.rm(claudeSkillsPath, { recursive: true, force: true });
177
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
178
- }
179
- }
180
- catch {
181
- // Directory doesn't exist, nothing to clean
182
- }
183
- // Clean up .codex/skills
184
- try {
185
- await fs.access(codexSkillsPath);
186
- if (dryRun) {
187
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CODEX_SKILLS_PATH}`, verbose, dryRun);
188
- }
189
- else {
190
- await fs.rm(codexSkillsPath, { recursive: true, force: true });
191
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CODEX_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
192
- }
193
- }
194
- catch {
195
- // Directory doesn't exist, nothing to clean
196
- }
197
- // Clean up .opencode/skills
198
- try {
199
- await fs.access(opencodeSkillsPath);
200
- if (dryRun) {
201
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun);
202
- }
203
- else {
204
- await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
205
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
206
- }
207
- }
208
- catch {
209
- // Directory doesn't exist, nothing to clean
210
- }
211
- // Clean up .pi/skills
212
- try {
213
- await fs.access(piSkillsPath);
214
- if (dryRun) {
215
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.PI_SKILLS_PATH}`, verbose, dryRun);
216
- }
217
- else {
218
- await fs.rm(piSkillsPath, { recursive: true, force: true });
219
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.PI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
220
- }
221
- }
222
- catch {
223
- // Directory doesn't exist, nothing to clean
224
- }
225
- // Clean up .agents/skills
226
- try {
227
- await fs.access(gooseSkillsPath);
228
- if (dryRun) {
229
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun);
230
- }
231
- else {
232
- await fs.rm(gooseSkillsPath, { recursive: true, force: true });
233
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
234
- }
235
- }
236
- catch {
237
- // Directory doesn't exist, nothing to clean
238
- }
239
- // Clean up .vibe/skills
240
- try {
241
- await fs.access(vibeSkillsPath);
242
- if (dryRun) {
243
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun);
244
- }
245
- else {
246
- await fs.rm(vibeSkillsPath, { recursive: true, force: true });
247
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
248
- }
249
- }
250
- catch {
251
- // Directory doesn't exist, nothing to clean
252
- }
253
- // Clean up .roo/skills
254
- try {
255
- await fs.access(rooSkillsPath);
256
- if (dryRun) {
257
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ROO_SKILLS_PATH}`, verbose, dryRun);
258
- }
259
- else {
260
- await fs.rm(rooSkillsPath, { recursive: true, force: true });
261
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ROO_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
262
- }
263
- }
264
- catch {
265
- // Directory doesn't exist, nothing to clean
266
- }
267
- // Clean up .gemini/skills
268
- try {
269
- await fs.access(geminiSkillsPath);
270
- if (dryRun) {
271
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GEMINI_SKILLS_PATH}`, verbose, dryRun);
272
- }
273
- else {
274
- await fs.rm(geminiSkillsPath, { recursive: true, force: true });
275
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GEMINI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
276
- }
277
- }
278
- catch {
279
- // Directory doesn't exist, nothing to clean
280
- }
281
- // Clean up .junie/skills
169
+ function isMissingPathError(error) {
170
+ return (typeof error === 'object' &&
171
+ error !== null &&
172
+ 'code' in error &&
173
+ error.code === 'ENOENT');
174
+ }
175
+ async function cleanupSkillsDirectory(projectRoot, relativePath, dryRun, verbose) {
176
+ const targetPath = path.join(projectRoot, relativePath);
282
177
  try {
283
- await fs.access(junieSkillsPath);
284
- if (dryRun) {
285
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.JUNIE_SKILLS_PATH}`, verbose, dryRun);
286
- }
287
- else {
288
- await fs.rm(junieSkillsPath, { recursive: true, force: true });
289
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.JUNIE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
290
- }
291
- }
292
- catch {
293
- // Directory doesn't exist, nothing to clean
178
+ await fs.access(targetPath);
294
179
  }
295
- // Clean up .cursor/skills
296
- try {
297
- await fs.access(cursorSkillsPath);
298
- if (dryRun) {
299
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CURSOR_SKILLS_PATH}`, verbose, dryRun);
300
- }
301
- else {
302
- await fs.rm(cursorSkillsPath, { recursive: true, force: true });
303
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CURSOR_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
180
+ catch (error) {
181
+ if (isMissingPathError(error)) {
182
+ return;
304
183
  }
184
+ throw error;
305
185
  }
306
- catch {
307
- // Directory doesn't exist, nothing to clean
186
+ if (dryRun) {
187
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${relativePath}`, verbose, dryRun);
188
+ return;
308
189
  }
309
- // Clean up .windsurf/skills
310
- try {
311
- await fs.access(windsurfSkillsPath);
312
- if (dryRun) {
313
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.WINDSURF_SKILLS_PATH}`, verbose, dryRun);
314
- }
315
- else {
316
- await fs.rm(windsurfSkillsPath, { recursive: true, force: true });
317
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.WINDSURF_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
318
- }
190
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(targetPath, projectRoot, 'Refusing to remove skills directory through symlinked path');
191
+ await fs.rm(targetPath, { recursive: true, force: true });
192
+ (0, constants_1.logVerboseInfo)(`Removed ${relativePath} (skills disabled)`, verbose, dryRun);
193
+ }
194
+ async function createTempSkillsDir(parentDir, containmentRoot) {
195
+ if (containmentRoot) {
196
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(parentDir, containmentRoot, 'Refusing to create temporary skills directory through symlinked path');
319
197
  }
320
- catch {
321
- // Directory doesn't exist, nothing to clean
198
+ return fs.mkdtemp(path.join(parentDir, 'skills.tmp-'));
199
+ }
200
+ const TRANSIENT_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'ENOTEMPTY']);
201
+ const RENAME_RETRY_ATTEMPTS = 5;
202
+ const RENAME_RETRY_DELAY_MS = 50;
203
+ function isTransientRenameError(error) {
204
+ return (typeof error === 'object' &&
205
+ error !== null &&
206
+ 'code' in error &&
207
+ typeof error.code === 'string' &&
208
+ TRANSIENT_RENAME_ERROR_CODES.has(error.code));
209
+ }
210
+ function wait(ms) {
211
+ return new Promise((resolve) => setTimeout(resolve, ms));
212
+ }
213
+ async function replaceSkillsDirectory(tempDir, targetDir, fsOps = fs, containmentRoot) {
214
+ if (containmentRoot) {
215
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(targetDir, containmentRoot, 'Refusing to replace skills directory through symlinked path');
322
216
  }
323
- // Clean up .factory/skills
324
- try {
325
- await fs.access(factorySkillsPath);
326
- if (dryRun) {
327
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.FACTORY_SKILLS_PATH}`, verbose, dryRun);
217
+ let lastError;
218
+ for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt += 1) {
219
+ try {
220
+ await fsOps.rename(tempDir, targetDir);
221
+ return;
328
222
  }
329
- else {
330
- await fs.rm(factorySkillsPath, { recursive: true, force: true });
331
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.FACTORY_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
223
+ catch (error) {
224
+ lastError = error;
225
+ if (!isTransientRenameError(error) || attempt === RENAME_RETRY_ATTEMPTS) {
226
+ break;
227
+ }
228
+ await wait(RENAME_RETRY_DELAY_MS * attempt);
332
229
  }
333
230
  }
334
- catch {
335
- // Directory doesn't exist, nothing to clean
336
- }
337
- // Clean up .agent/skills
338
- try {
339
- await fs.access(antigravitySkillsPath);
340
- if (dryRun) {
341
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ANTIGRAVITY_SKILLS_PATH}`, verbose, dryRun);
342
- }
343
- else {
344
- await fs.rm(antigravitySkillsPath, { recursive: true, force: true });
345
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ANTIGRAVITY_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
346
- }
231
+ if (isTransientRenameError(lastError)) {
232
+ await fsOps.cp(tempDir, targetDir, { recursive: true, force: true });
233
+ await fsOps.rm(tempDir, { recursive: true, force: true });
234
+ return;
347
235
  }
348
- catch {
349
- // Directory doesn't exist, nothing to clean
236
+ throw lastError;
237
+ }
238
+ /**
239
+ * Cleans up skills directories when skills are disabled.
240
+ * This ensures that stale skills from previous runs don't persist when skills are turned off.
241
+ */
242
+ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
243
+ for (const skillPath of SKILL_TARGET_PATHS) {
244
+ await cleanupSkillsDirectory(projectRoot, skillPath, dryRun, verbose);
350
245
  }
351
246
  }
352
247
  /**
@@ -416,7 +311,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
416
311
  await propagateSkillsForPi(projectRoot, { dryRun });
417
312
  }
418
313
  if (selectedTargets.has('goose')) {
419
- (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose and Amp`, verbose, dryRun);
314
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose, Amp and Zed`, verbose, dryRun);
420
315
  await propagateSkillsForGoose(projectRoot, { dryRun });
421
316
  }
422
317
  if (selectedTargets.has('vibe')) {
@@ -476,7 +371,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
476
371
  // Ensure .claude directory exists
477
372
  await fs.mkdir(claudeDir, { recursive: true });
478
373
  // Use atomic replace: copy to temp, then rename
479
- const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`);
374
+ const tempDir = await createTempSkillsDir(claudeDir, projectRoot);
480
375
  try {
481
376
  // Copy to temp directory
482
377
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -489,7 +384,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
489
384
  // Target didn't exist, that's fine
490
385
  }
491
386
  // Rename temp to target
492
- await fs.rename(tempDir, claudeSkillsPath);
387
+ await replaceSkillsDirectory(tempDir, claudeSkillsPath, fs, projectRoot);
493
388
  }
494
389
  catch (error) {
495
390
  // Clean up temp directory on error
@@ -504,14 +399,14 @@ async function propagateSkillsForClaude(projectRoot, options) {
504
399
  return [];
505
400
  }
506
401
  /**
507
- * Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .codex/skills.
402
+ * Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .agents/skills.
508
403
  * Uses atomic replace to ensure safe overwriting of existing skills.
509
404
  * Returns dry-run steps if dryRun is true, otherwise returns empty array.
510
405
  */
511
406
  async function propagateSkillsForCodex(projectRoot, options) {
512
407
  const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
513
- const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
514
- const codexDir = path.dirname(codexSkillsPath);
408
+ const agentsSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
409
+ const agentsDir = path.dirname(agentsSkillsPath);
515
410
  // Check if source skills directory exists
516
411
  try {
517
412
  await fs.access(skillsDir);
@@ -523,23 +418,23 @@ async function propagateSkillsForCodex(projectRoot, options) {
523
418
  if (options.dryRun) {
524
419
  return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CODEX_SKILLS_PATH}`];
525
420
  }
526
- // Ensure .codex directory exists
527
- await fs.mkdir(codexDir, { recursive: true });
421
+ // Ensure .agents directory exists
422
+ await fs.mkdir(agentsDir, { recursive: true });
528
423
  // Use atomic replace: copy to temp, then rename
529
- const tempDir = path.join(codexDir, `skills.tmp-${Date.now()}`);
424
+ const tempDir = await createTempSkillsDir(agentsDir, projectRoot);
530
425
  try {
531
426
  // Copy to temp directory
532
427
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
533
428
  // Atomically replace the target
534
429
  // First, remove existing target if it exists
535
430
  try {
536
- await fs.rm(codexSkillsPath, { recursive: true, force: true });
431
+ await fs.rm(agentsSkillsPath, { recursive: true, force: true });
537
432
  }
538
433
  catch {
539
434
  // Target didn't exist, that's fine
540
435
  }
541
436
  // Rename temp to target
542
- await fs.rename(tempDir, codexSkillsPath);
437
+ await replaceSkillsDirectory(tempDir, agentsSkillsPath, fs, projectRoot);
543
438
  }
544
439
  catch (error) {
545
440
  // Clean up temp directory on error
@@ -576,7 +471,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
576
471
  // Ensure .opencode directory exists
577
472
  await fs.mkdir(opencodeDir, { recursive: true });
578
473
  // Use atomic replace: copy to temp, then rename
579
- const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`);
474
+ const tempDir = await createTempSkillsDir(opencodeDir, projectRoot);
580
475
  try {
581
476
  // Copy to temp directory
582
477
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -589,7 +484,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
589
484
  // Target didn't exist, that's fine
590
485
  }
591
486
  // Rename temp to target
592
- await fs.rename(tempDir, opencodeSkillsPath);
487
+ await replaceSkillsDirectory(tempDir, opencodeSkillsPath, fs, projectRoot);
593
488
  }
594
489
  catch (error) {
595
490
  // Clean up temp directory on error
@@ -626,7 +521,7 @@ async function propagateSkillsForPi(projectRoot, options) {
626
521
  // Ensure .pi directory exists
627
522
  await fs.mkdir(piDir, { recursive: true });
628
523
  // Use atomic replace: copy to temp, then rename
629
- const tempDir = path.join(piDir, `skills.tmp-${Date.now()}`);
524
+ const tempDir = await createTempSkillsDir(piDir, projectRoot);
630
525
  try {
631
526
  // Copy to temp directory
632
527
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -639,7 +534,7 @@ async function propagateSkillsForPi(projectRoot, options) {
639
534
  // Target didn't exist, that's fine
640
535
  }
641
536
  // Rename temp to target
642
- await fs.rename(tempDir, piSkillsPath);
537
+ await replaceSkillsDirectory(tempDir, piSkillsPath, fs, projectRoot);
643
538
  }
644
539
  catch (error) {
645
540
  // Clean up temp directory on error
@@ -676,7 +571,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
676
571
  // Ensure .agents directory exists
677
572
  await fs.mkdir(gooseDir, { recursive: true });
678
573
  // Use atomic replace: copy to temp, then rename
679
- const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`);
574
+ const tempDir = await createTempSkillsDir(gooseDir, projectRoot);
680
575
  try {
681
576
  // Copy to temp directory
682
577
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -689,7 +584,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
689
584
  // Target didn't exist, that's fine
690
585
  }
691
586
  // Rename temp to target
692
- await fs.rename(tempDir, gooseSkillsPath);
587
+ await replaceSkillsDirectory(tempDir, gooseSkillsPath, fs, projectRoot);
693
588
  }
694
589
  catch (error) {
695
590
  // Clean up temp directory on error
@@ -726,7 +621,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
726
621
  // Ensure .vibe directory exists
727
622
  await fs.mkdir(vibeDir, { recursive: true });
728
623
  // Use atomic replace: copy to temp, then rename
729
- const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`);
624
+ const tempDir = await createTempSkillsDir(vibeDir, projectRoot);
730
625
  try {
731
626
  // Copy to temp directory
732
627
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -739,7 +634,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
739
634
  // Target didn't exist, that's fine
740
635
  }
741
636
  // Rename temp to target
742
- await fs.rename(tempDir, vibeSkillsPath);
637
+ await replaceSkillsDirectory(tempDir, vibeSkillsPath, fs, projectRoot);
743
638
  }
744
639
  catch (error) {
745
640
  // Clean up temp directory on error
@@ -776,7 +671,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
776
671
  // Ensure .roo directory exists
777
672
  await fs.mkdir(rooDir, { recursive: true });
778
673
  // Use atomic replace: copy to temp, then rename
779
- const tempDir = path.join(rooDir, `skills.tmp-${Date.now()}`);
674
+ const tempDir = await createTempSkillsDir(rooDir, projectRoot);
780
675
  try {
781
676
  // Copy to temp directory
782
677
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -789,7 +684,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
789
684
  // Target didn't exist, that's fine
790
685
  }
791
686
  // Rename temp to target
792
- await fs.rename(tempDir, rooSkillsPath);
687
+ await replaceSkillsDirectory(tempDir, rooSkillsPath, fs, projectRoot);
793
688
  }
794
689
  catch (error) {
795
690
  // Clean up temp directory on error
@@ -826,7 +721,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
826
721
  // Ensure .gemini directory exists
827
722
  await fs.mkdir(geminiDir, { recursive: true });
828
723
  // Use atomic replace: copy to temp, then rename
829
- const tempDir = path.join(geminiDir, `skills.tmp-${Date.now()}`);
724
+ const tempDir = await createTempSkillsDir(geminiDir, projectRoot);
830
725
  try {
831
726
  // Copy to temp directory
832
727
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -839,7 +734,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
839
734
  // Target didn't exist, that's fine
840
735
  }
841
736
  // Rename temp to target
842
- await fs.rename(tempDir, geminiSkillsPath);
737
+ await replaceSkillsDirectory(tempDir, geminiSkillsPath, fs, projectRoot);
843
738
  }
844
739
  catch (error) {
845
740
  // Clean up temp directory on error
@@ -872,7 +767,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
872
767
  return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.JUNIE_SKILLS_PATH}`];
873
768
  }
874
769
  await fs.mkdir(junieDir, { recursive: true });
875
- const tempDir = path.join(junieDir, `skills.tmp-${Date.now()}`);
770
+ const tempDir = await createTempSkillsDir(junieDir, projectRoot);
876
771
  try {
877
772
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
878
773
  try {
@@ -881,7 +776,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
881
776
  catch {
882
777
  // Target didn't exist, that's fine
883
778
  }
884
- await fs.rename(tempDir, junieSkillsPath);
779
+ await replaceSkillsDirectory(tempDir, junieSkillsPath, fs, projectRoot);
885
780
  }
886
781
  catch (error) {
887
782
  try {
@@ -917,7 +812,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
917
812
  // Ensure .cursor directory exists
918
813
  await fs.mkdir(cursorDir, { recursive: true });
919
814
  // Use atomic replace: copy to temp, then rename
920
- const tempDir = path.join(cursorDir, `skills.tmp-${Date.now()}`);
815
+ const tempDir = await createTempSkillsDir(cursorDir, projectRoot);
921
816
  try {
922
817
  // Copy to temp directory
923
818
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -930,7 +825,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
930
825
  // Target didn't exist, that's fine
931
826
  }
932
827
  // Rename temp to target
933
- await fs.rename(tempDir, cursorSkillsPath);
828
+ await replaceSkillsDirectory(tempDir, cursorSkillsPath, fs, projectRoot);
934
829
  }
935
830
  catch (error) {
936
831
  // Clean up temp directory on error
@@ -967,7 +862,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
967
862
  // Ensure .windsurf directory exists
968
863
  await fs.mkdir(windsurfDir, { recursive: true });
969
864
  // Use atomic replace: copy to temp, then rename
970
- const tempDir = path.join(windsurfDir, `skills.tmp-${Date.now()}`);
865
+ const tempDir = await createTempSkillsDir(windsurfDir, projectRoot);
971
866
  try {
972
867
  // Copy to temp directory
973
868
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -980,7 +875,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
980
875
  // Target didn't exist, that's fine
981
876
  }
982
877
  // Rename temp to target
983
- await fs.rename(tempDir, windsurfSkillsPath);
878
+ await replaceSkillsDirectory(tempDir, windsurfSkillsPath, fs, projectRoot);
984
879
  }
985
880
  catch (error) {
986
881
  // Clean up temp directory on error
@@ -1017,7 +912,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
1017
912
  // Ensure .factory directory exists
1018
913
  await fs.mkdir(factoryDir, { recursive: true });
1019
914
  // Use atomic replace: copy to temp, then rename
1020
- const tempDir = path.join(factoryDir, `skills.tmp-${Date.now()}`);
915
+ const tempDir = await createTempSkillsDir(factoryDir, projectRoot);
1021
916
  try {
1022
917
  // Copy to temp directory
1023
918
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -1030,7 +925,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
1030
925
  // Target didn't exist, that's fine
1031
926
  }
1032
927
  // Rename temp to target
1033
- await fs.rename(tempDir, factorySkillsPath);
928
+ await replaceSkillsDirectory(tempDir, factorySkillsPath, fs, projectRoot);
1034
929
  }
1035
930
  catch (error) {
1036
931
  // Clean up temp directory on error
@@ -1069,7 +964,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
1069
964
  // Ensure .agent directory exists
1070
965
  await fs.mkdir(antigravityDir, { recursive: true });
1071
966
  // Use atomic replace: copy to temp, then rename
1072
- const tempDir = path.join(antigravityDir, `skills.tmp-${Date.now()}`);
967
+ const tempDir = await createTempSkillsDir(antigravityDir, projectRoot);
1073
968
  try {
1074
969
  // Copy to temp directory
1075
970
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -1082,7 +977,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
1082
977
  // Target didn't exist, that's fine
1083
978
  }
1084
979
  // Rename temp to target
1085
- await fs.rename(tempDir, antigravitySkillsPath);
980
+ await replaceSkillsDirectory(tempDir, antigravitySkillsPath, fs, projectRoot);
1086
981
  }
1087
982
  catch (error) {
1088
983
  // Clean up temp directory on error
@@ -0,0 +1,26 @@
1
+ import { SkillInfo } from '../types';
2
+ /**
3
+ * Checks if a directory contains a SKILL.md file.
4
+ */
5
+ export declare function hasSkillMd(dirPath: string): Promise<boolean>;
6
+ /**
7
+ * Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
8
+ */
9
+ export declare function isGroupingDir(dirPath: string): Promise<boolean>;
10
+ /**
11
+ * Walks the skills tree and discovers all skills.
12
+ * Returns skills and any validation warnings.
13
+ */
14
+ export declare function walkSkillsTree(root: string): Promise<{
15
+ skills: SkillInfo[];
16
+ warnings: string[];
17
+ }>;
18
+ /**
19
+ * Formats validation warnings for display.
20
+ */
21
+ export declare function formatValidationWarnings(warnings: string[]): string;
22
+ /**
23
+ * Copies the skills directory to the destination, preserving structure.
24
+ * Creates the destination directory if it doesn't exist.
25
+ */
26
+ export declare function copySkillsDirectory(srcDir: string, destDir: string): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import type { SubagentInfo } from '../types';
2
+ import type { IAgent } from '../agents/IAgent';
3
+ /**
4
+ * Discovers subagent definitions in `.ruler/agents/`.
5
+ * Each `.md` file is parsed for YAML frontmatter (name, description, …).
6
+ * Files that fail validation are dropped from the returned list and
7
+ * reported via warnings.
8
+ */
9
+ export declare function discoverSubagents(projectRoot: string): Promise<{
10
+ subagents: SubagentInfo[];
11
+ warnings: string[];
12
+ }>;
13
+ type SubagentTarget = 'claude' | 'cursor' | 'codex' | 'copilot';
14
+ /**
15
+ * Returns which native subagent targets are reachable through the supplied
16
+ * agent list. An agent only contributes to a target when it implements
17
+ * `supportsNativeSubagents()` returning true.
18
+ */
19
+ export declare function getSelectedSubagentTargets(agents: IAgent[]): Set<SubagentTarget>;
20
+ /**
21
+ * Returns absolute paths that subagent propagation may generate, for the
22
+ * supplied agents, used for `.gitignore` integration.
23
+ */
24
+ export declare function getSubagentsGitignorePaths(projectRoot: string, agents: IAgent[]): Promise<string[]>;
25
+ /**
26
+ * Test-only hook to reset the once-per-process experimental warning state.
27
+ */
28
+ export declare function _resetExperimentalWarningForTests(): void;
29
+ interface PropagateOptions {
30
+ dryRun: boolean;
31
+ verbose?: boolean;
32
+ }
33
+ export declare function propagateSubagentsForClaude(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
34
+ export declare function propagateSubagentsForCursor(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
35
+ export declare function propagateSubagentsForCodex(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
36
+ export declare function propagateSubagentsForCopilot(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
37
+ export declare function propagateSubagents(projectRoot: string, agents: IAgent[], subagentsEnabled: boolean, cleanupOrphaned: boolean, verbose: boolean, dryRun: boolean): Promise<void>;
38
+ export {};