@intellectronica/ruler 0.3.37 → 0.3.39

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 CHANGED
@@ -54,40 +54,40 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
54
54
 
55
55
  ## Supported AI Agents
56
56
 
57
- | Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location |
58
- | ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- |
59
- | AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - |
60
- | GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` |
61
- | Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` |
62
- | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.codex/skills/` |
63
- | Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` |
64
- | Jules | `AGENTS.md` | - | - |
65
- | Cursor | `AGENTS.md` | `.cursor/mcp.json` | `.cursor/skills/` |
66
- | Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` | - |
67
- | Cline | `.clinerules` | - | - |
68
- | Crush | `CRUSH.md` | `.crush.json` | - |
69
- | Amp | `AGENTS.md` | - | `.agents/skills/` |
70
- | Antigravity | `.agent/rules/ruler.md` | - | `.agent/skills/` |
71
- | Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` | - |
72
- | Aider | `AGENTS.md`, `.aider.conf.yml` | `.mcp.json` | - |
73
- | Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - |
74
- | Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - |
75
- | Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` |
76
- | Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | `.junie/skills/` |
77
- | AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - |
78
- | Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` |
79
- | OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` |
80
- | Goose | `.goosehints` | - | `.agents/skills/` |
81
- | Qwen Code | `AGENTS.md` | `.qwen/settings.json` | - |
82
- | RooCode | `AGENTS.md` | `.roo/mcp.json` | `.roo/skills/` |
83
- | Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) | - |
84
- | Trae AI | `.trae/rules/project_rules.md` | - | - |
85
- | Warp | `WARP.md` | - | - |
86
- | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` | - |
87
- | Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) | - |
88
- | Factory Droid | `AGENTS.md` | `.factory/mcp.json` | `.factory/skills/` |
89
- | Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` | `.vibe/skills/` |
90
- | JetBrains AI Assistant | `.aiassistant/rules/AGENTS.md` | - | - |
57
+ | Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location | Subagents Support / Location |
58
+ | ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- | ---------------------------- |
59
+ | AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - | - |
60
+ | GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` | `.github/agents/` |
61
+ | Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` | `.claude/agents/` |
62
+ | OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.codex/skills/` | `.codex/agents/` (`.toml`) |
63
+ | Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` | - |
64
+ | Jules | `AGENTS.md` | - | - | - |
65
+ | Cursor | `AGENTS.md` | `.cursor/mcp.json` | `.cursor/skills/` | `.cursor/agents/` |
66
+ | Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` | `.windsurf/skills/` | - |
67
+ | Cline | `.clinerules` | - | - | - |
68
+ | Crush | `CRUSH.md` | `.crush.json` | - | - |
69
+ | Amp | `AGENTS.md` | - | `.agents/skills/` | - |
70
+ | Antigravity | `.agent/rules/ruler.md` | - | `.agent/skills/` | - |
71
+ | Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` | - | - |
72
+ | Aider | `AGENTS.md`, `.aider.conf.yml` | `.mcp.json` | - | - |
73
+ | Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - | - |
74
+ | Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - | - |
75
+ | Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` | - |
76
+ | Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | `.junie/skills/` | - |
77
+ | AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - | - |
78
+ | Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` | - |
79
+ | OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` | - |
80
+ | Goose | `.goosehints` | - | `.agents/skills/` | - |
81
+ | Qwen Code | `AGENTS.md` | `.qwen/settings.json` | - | - |
82
+ | RooCode | `AGENTS.md` | `.roo/mcp.json` | `.roo/skills/` | - |
83
+ | Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) | - | - |
84
+ | Trae AI | `.trae/rules/project_rules.md` | - | - | - |
85
+ | Warp | `WARP.md` | - | - | - |
86
+ | Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` | - | - |
87
+ | Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) | - | - |
88
+ | Factory Droid | `AGENTS.md` | `.factory/mcp.json` | `.factory/skills/` | - |
89
+ | Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` | `.vibe/skills/` | - |
90
+ | JetBrains AI Assistant | `.aiassistant/rules/AGENTS.md` | - | - | - |
91
91
 
92
92
  ## Getting Started
93
93
 
@@ -241,9 +241,12 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
241
241
  | `--gitignore-local` | Write managed ignore entries to `.git/info/exclude` instead. |
242
242
  | `--nested` | Enable nested rule loading (default: inherit from config or disabled). |
243
243
  | `--no-nested` | Disable nested rule loading even if `nested = true` in config. |
244
- | `--backup` | Toggle creation of `.bak` backup files (default: enabled). |
244
+ | `--backup` | Enable creation of `.bak` backup files (default: enabled). |
245
+ | `--no-backup` | Disable creation of `.bak` backup files. |
245
246
  | `--skills` | Enable skills support (experimental, default: enabled). |
246
247
  | `--no-skills` | Disable skills support. |
248
+ | `--subagents` | Enable subagents support (experimental, default: enabled). |
249
+ | `--no-subagents` | Disable subagents support. |
247
250
  | `--dry-run` | Preview changes without writing files. |
248
251
  | `--local-only` | Skip `$XDG_CONFIG_HOME` when looking for configuration. |
249
252
  | `--verbose` / `-v` | Display detailed output during execution. |
@@ -596,6 +599,7 @@ Skills are specialized knowledge packages that extend AI agent capabilities with
596
599
  - **Gemini CLI**: `.gemini/skills/`
597
600
  - **Junie**: `.junie/skills/`
598
601
  - **Cursor**: `.cursor/skills/`
602
+ - **Windsurf**: `.windsurf/skills/`
599
603
 
600
604
  ### Skills Directory Structure
601
605
 
@@ -662,12 +666,13 @@ When skills support is enabled and gitignore integration is active, Ruler automa
662
666
  - `.gemini/skills/` (for Gemini CLI)
663
667
  - `.junie/skills/` (for Junie)
664
668
  - `.cursor/skills/` (for Cursor)
669
+ - `.windsurf/skills/` (for Windsurf)
665
670
 
666
671
  to your `.gitignore` file within the managed Ruler block.
667
672
 
668
673
  ### Requirements
669
674
 
670
- - **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Junie, Cursor): No additional requirements.
675
+ - **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Junie, Cursor, Windsurf): No additional requirements.
671
676
 
672
677
  ### Validation
673
678
 
@@ -722,8 +727,145 @@ ruler apply
722
727
  # - Gemini CLI: .gemini/skills/my-skill/
723
728
  # - Junie: .junie/skills/my-skill/
724
729
  # - Cursor: .cursor/skills/my-skill/
730
+ # - Windsurf: .windsurf/skills/my-skill/
725
731
  ```
726
732
 
733
+ ## Subagents Support (Experimental)
734
+
735
+ > **⚠️ Experimental:** Subagents support is experimental and behavior may change in future releases.
736
+
737
+ Ruler can distribute named, delegatable **subagents** from a single source of truth (`.ruler/agents/`) to each agent's native subagent location. Each source file is one Markdown file with YAML frontmatter; Ruler transforms it into the format the target agent expects.
738
+
739
+ ### How It Works
740
+
741
+ For agents with a native subagent primitive, Ruler writes one file per subagent into the target directory:
742
+
743
+ | Agent | Target location | Format |
744
+ | ----------------- | ------------------------------ | ------ |
745
+ | Claude Code | `.claude/agents/<name>.md` | Markdown + YAML frontmatter |
746
+ | Cursor | `.cursor/agents/<name>.md` | Markdown + YAML frontmatter |
747
+ | OpenAI Codex CLI | `.codex/agents/<name>.toml` | TOML (one self-contained file per agent) |
748
+ | GitHub Copilot | `.github/agents/<name>.md` | Markdown + YAML frontmatter |
749
+
750
+ Other agents (Windsurf, RooCode, Aider, Gemini CLI, …) do not yet have a comparable native subagent primitive and are skipped with a warning. Subagent propagation will be added when those agents ship a comparable file format.
751
+
752
+ ### Source Format
753
+
754
+ Author each subagent as `.ruler/agents/<name>.md`:
755
+
756
+ ```markdown
757
+ ---
758
+ name: code-reviewer
759
+ description: Use PROACTIVELY after a feature/fix is implemented. Reviews against SOLID/DRY/KISS. Read-only.
760
+ tools: [Read, Grep, Glob, Bash]
761
+ model: inherit
762
+ readonly: true
763
+ is_background: false
764
+ ---
765
+
766
+ # Code Reviewer
767
+
768
+ You operate in a fresh context window with read-only access. Your job is to
769
+ review the diff and surrounding code against the design principles and return
770
+ a structured verdict.
771
+ ```
772
+
773
+ **Required frontmatter fields:**
774
+
775
+ | Field | Type | Notes |
776
+ | ------------- | ------ | ----------------------------------------------------- |
777
+ | `name` | string | Must match the filename stem (`code-reviewer.md` → `name: code-reviewer`). |
778
+ | `description` | string | When the parent agent should delegate to this subagent. |
779
+
780
+ **Optional frontmatter fields:**
781
+
782
+ | Field | Type | Used by | Default behavior |
783
+ | --------------- | ---------------- | ------------------------------------------------ | --------------------------------------------- |
784
+ | `tools` | string[] | Claude (verbatim), Copilot (mapped to aliases) | Cursor / Codex ignore; omitted if absent. |
785
+ | `model` | string | All four targets | Cursor defaults to `inherit`; others omit. |
786
+ | `readonly` | boolean | Cursor (verbatim), Codex (`sandbox_mode`), Copilot (`disable-model-invocation`) | Defaults to `false` for Cursor; omitted otherwise. |
787
+ | `is_background` | boolean | Cursor only | Defaults to `false` for Cursor. |
788
+
789
+ For GitHub Copilot, source `tools` (Claude vocabulary: `Read`, `Grep`, `Bash`, …) are translated to Copilot's aliases (`read`, `search`, `execute`, …). Tools that do not have a Copilot equivalent are dropped silently on a normal apply; pass `--verbose` (or use `--dry-run` to preview) to see which tools were dropped.
790
+
791
+ ### Configuration
792
+
793
+ Subagents are enabled by default. Toggle them via CLI flag or `ruler.toml`:
794
+
795
+ ```bash
796
+ ruler apply --no-subagents # disable subagent propagation for one run
797
+ ```
798
+
799
+ ```toml
800
+ # .ruler/ruler.toml
801
+ [subagents]
802
+ enabled = false
803
+ ```
804
+
805
+ CLI flags take precedence over `ruler.toml`, which takes precedence over the default (enabled).
806
+
807
+ ### Validation
808
+
809
+ Source files are validated at discovery time:
810
+
811
+ - Files without YAML frontmatter are skipped with a warning.
812
+ - Files missing required `name` or `description` are skipped with a warning.
813
+ - Files where `name` does not match the filename stem are skipped with a warning.
814
+ - Unknown frontmatter keys are dropped (not errored).
815
+
816
+ ### Dry-Run Mode
817
+
818
+ Use `--dry-run` to preview which files would be written without touching disk.
819
+
820
+ ### `.gitignore` Integration
821
+
822
+ When subagents are enabled, the four target directories are added to the Ruler-managed block of `.gitignore`:
823
+
824
+ ```
825
+ .claude/agents/
826
+ .cursor/agents/
827
+ .codex/agents/
828
+ .github/agents/
829
+ ```
830
+
831
+ Use `--no-gitignore` to opt out.
832
+
833
+ ### Cleanup
834
+
835
+ Subagent propagation does **not** currently have explicit `ruler revert` support. To remove generated subagent directories, set `[subagents] enabled = false` (or pass `--no-subagents`) and run `ruler apply` once. Cleanup will run for all four targets even if no source `.ruler/agents/` directory exists.
836
+
837
+ ### Example Workflow
838
+
839
+ ```bash
840
+ # 1. Author a subagent in your project
841
+ mkdir -p .ruler/agents
842
+ cat > .ruler/agents/code-reviewer.md << 'EOF'
843
+ ---
844
+ name: code-reviewer
845
+ description: Reviews changes against SOLID/DRY/KISS
846
+ tools: [Read, Grep, Glob]
847
+ readonly: true
848
+ ---
849
+
850
+ You review code changes for quality.
851
+ EOF
852
+
853
+ # 2. Apply (subagents enabled by default)
854
+ ruler apply
855
+
856
+ # 3. The subagent is now available in each agent's native location:
857
+ # - Claude Code: .claude/agents/code-reviewer.md
858
+ # - Cursor: .cursor/agents/code-reviewer.md
859
+ # - Codex CLI: .codex/agents/code-reviewer.toml
860
+ # - GitHub Copilot: .github/agents/code-reviewer.md
861
+ ```
862
+
863
+ ### Limitations
864
+
865
+ - **No explicit revert command.** Cleanup happens via `[subagents] enabled = false` on a subsequent `apply`.
866
+ - **Atomic replace, not merge.** Ruler regenerates each agent's subagent directory from the source on every apply. Manual edits to generated files will be overwritten.
867
+ - **No support yet for agents without a native subagent primitive.** Windsurf, RooCode, Aider, Gemini CLI, and others are skipped with a warning. Propagation will be added when those agents ship a comparable file format.
868
+
727
869
  ## `.gitignore` Integration
728
870
 
729
871
  Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
@@ -58,5 +58,8 @@ class ClaudeAgent extends AbstractAgent_1.AbstractAgent {
58
58
  supportsNativeSkills() {
59
59
  return true;
60
60
  }
61
+ supportsNativeSubagents() {
62
+ return true;
63
+ }
61
64
  }
62
65
  exports.ClaudeAgent = ClaudeAgent;
@@ -149,5 +149,8 @@ class CodexCliAgent {
149
149
  supportsNativeSkills() {
150
150
  return true;
151
151
  }
152
+ supportsNativeSubagents() {
153
+ return true;
154
+ }
152
155
  }
153
156
  exports.CodexCliAgent = CodexCliAgent;
@@ -42,5 +42,8 @@ class CopilotAgent {
42
42
  supportsNativeSkills() {
43
43
  return true;
44
44
  }
45
+ supportsNativeSubagents() {
46
+ return true;
47
+ }
45
48
  }
46
49
  exports.CopilotAgent = CopilotAgent;
@@ -33,5 +33,8 @@ class CursorAgent extends AgentsMdAgent_1.AgentsMdAgent {
33
33
  supportsNativeSkills() {
34
34
  return true;
35
35
  }
36
+ supportsNativeSubagents() {
37
+ return true;
38
+ }
36
39
  }
37
40
  exports.CursorAgent = CursorAgent;
@@ -23,5 +23,8 @@ class WindsurfAgent extends AgentsMdAgent_1.AgentsMdAgent {
23
23
  supportsMcpRemote() {
24
24
  return true;
25
25
  }
26
+ supportsNativeSkills() {
27
+ return true;
28
+ }
26
29
  }
27
30
  exports.WindsurfAgent = WindsurfAgent;
@@ -77,6 +77,10 @@ function run() {
77
77
  .option('skills', {
78
78
  type: 'boolean',
79
79
  description: 'Enable/disable skills support (experimental, default: enabled)',
80
+ })
81
+ .option('subagents', {
82
+ type: 'boolean',
83
+ description: 'Enable/disable subagents support (experimental, default: enabled)',
80
84
  });
81
85
  }, handlers_1.applyHandler)
82
86
  .command('init', 'Scaffold a .ruler directory with default files', (y) => {
@@ -114,8 +114,16 @@ async function applyHandler(argv) {
114
114
  else {
115
115
  skillsEnabled = undefined; // Let config/default decide
116
116
  }
117
+ // Determine subagents preference: CLI > TOML > Default (enabled)
118
+ let subagentsEnabled;
119
+ if (argv.subagents !== undefined) {
120
+ subagentsEnabled = argv.subagents;
121
+ }
122
+ else {
123
+ subagentsEnabled = undefined; // Let config/default decide
124
+ }
117
125
  try {
118
- await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup, skillsEnabled, gitignoreLocalPreference);
126
+ await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup, skillsEnabled, gitignoreLocalPreference, subagentsEnabled);
119
127
  console.log('Ruler apply completed successfully.');
120
128
  }
121
129
  catch (err) {
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SKILL_MD_FILENAME = exports.ANTIGRAVITY_SKILLS_PATH = exports.FACTORY_SKILLS_PATH = exports.CURSOR_SKILLS_PATH = exports.JUNIE_SKILLS_PATH = exports.GEMINI_SKILLS_PATH = exports.ROO_SKILLS_PATH = exports.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.PI_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
3
+ exports.COPILOT_SUBAGENTS_PATH = exports.CODEX_SUBAGENTS_PATH = exports.CURSOR_SUBAGENTS_PATH = exports.CLAUDE_SUBAGENTS_PATH = exports.RULER_SUBAGENTS_PATH = exports.SKILL_MD_FILENAME = exports.ANTIGRAVITY_SKILLS_PATH = exports.FACTORY_SKILLS_PATH = exports.WINDSURF_SKILLS_PATH = exports.CURSOR_SKILLS_PATH = exports.JUNIE_SKILLS_PATH = exports.GEMINI_SKILLS_PATH = exports.ROO_SKILLS_PATH = exports.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.PI_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
4
4
  exports.actionPrefix = actionPrefix;
5
5
  exports.createRulerError = createRulerError;
6
6
  exports.logVerbose = logVerbose;
@@ -62,6 +62,13 @@ exports.ROO_SKILLS_PATH = '.roo/skills';
62
62
  exports.GEMINI_SKILLS_PATH = '.gemini/skills';
63
63
  exports.JUNIE_SKILLS_PATH = '.junie/skills';
64
64
  exports.CURSOR_SKILLS_PATH = '.cursor/skills';
65
+ exports.WINDSURF_SKILLS_PATH = '.windsurf/skills';
65
66
  exports.FACTORY_SKILLS_PATH = '.factory/skills';
66
67
  exports.ANTIGRAVITY_SKILLS_PATH = '.agent/skills';
67
68
  exports.SKILL_MD_FILENAME = 'SKILL.md';
69
+ // Subagents-related constants
70
+ exports.RULER_SUBAGENTS_PATH = '.ruler/agents';
71
+ exports.CLAUDE_SUBAGENTS_PATH = '.claude/agents';
72
+ exports.CURSOR_SUBAGENTS_PATH = '.cursor/agents';
73
+ exports.CODEX_SUBAGENTS_PATH = '.codex/agents';
74
+ exports.COPILOT_SUBAGENTS_PATH = '.github/agents';
@@ -75,6 +75,11 @@ const rulerConfigSchema = zod_1.z.object({
75
75
  enabled: zod_1.z.boolean().optional(),
76
76
  })
77
77
  .optional(),
78
+ subagents: zod_1.z
79
+ .object({
80
+ enabled: zod_1.z.boolean().optional(),
81
+ })
82
+ .optional(),
78
83
  nested: zod_1.z.boolean().optional(),
79
84
  });
80
85
  /**
@@ -214,6 +219,15 @@ async function loadConfig(options) {
214
219
  if (typeof rawSkillsSection.enabled === 'boolean') {
215
220
  skillsConfig.enabled = rawSkillsSection.enabled;
216
221
  }
222
+ const rawSubagentsSection = raw.subagents &&
223
+ typeof raw.subagents === 'object' &&
224
+ !Array.isArray(raw.subagents)
225
+ ? raw.subagents
226
+ : {};
227
+ const subagentsConfig = {};
228
+ if (typeof rawSubagentsSection.enabled === 'boolean') {
229
+ subagentsConfig.enabled = rawSubagentsSection.enabled;
230
+ }
217
231
  const nestedDefined = typeof raw.nested === 'boolean';
218
232
  const nested = nestedDefined ? raw.nested : false;
219
233
  return {
@@ -223,6 +237,7 @@ async function loadConfig(options) {
223
237
  mcp: globalMcpConfig,
224
238
  gitignore: gitignoreConfig,
225
239
  skills: skillsConfig,
240
+ subagents: subagentsConfig,
226
241
  nested,
227
242
  nestedDefined,
228
243
  };
@@ -46,6 +46,7 @@ exports.propagateSkillsForRoo = propagateSkillsForRoo;
46
46
  exports.propagateSkillsForGemini = propagateSkillsForGemini;
47
47
  exports.propagateSkillsForJunie = propagateSkillsForJunie;
48
48
  exports.propagateSkillsForCursor = propagateSkillsForCursor;
49
+ exports.propagateSkillsForWindsurf = propagateSkillsForWindsurf;
49
50
  exports.propagateSkillsForFactory = propagateSkillsForFactory;
50
51
  exports.propagateSkillsForAntigravity = propagateSkillsForAntigravity;
51
52
  const path = __importStar(require("path"));
@@ -83,7 +84,7 @@ async function getSkillsGitignorePaths(projectRoot, agents) {
83
84
  return [];
84
85
  }
85
86
  // Import here to avoid circular dependency
86
- const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, PI_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, ROO_SKILLS_PATH, GEMINI_SKILLS_PATH, JUNIE_SKILLS_PATH, CURSOR_SKILLS_PATH, FACTORY_SKILLS_PATH, ANTIGRAVITY_SKILLS_PATH, } = await Promise.resolve().then(() => __importStar(require('../constants')));
87
+ const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, PI_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, ROO_SKILLS_PATH, GEMINI_SKILLS_PATH, JUNIE_SKILLS_PATH, CURSOR_SKILLS_PATH, WINDSURF_SKILLS_PATH, FACTORY_SKILLS_PATH, ANTIGRAVITY_SKILLS_PATH, } = await Promise.resolve().then(() => __importStar(require('../constants')));
87
88
  const selectedTargets = getSelectedSkillTargets(agents);
88
89
  const targetPaths = {
89
90
  claude: CLAUDE_SKILLS_PATH,
@@ -96,6 +97,7 @@ async function getSkillsGitignorePaths(projectRoot, agents) {
96
97
  gemini: GEMINI_SKILLS_PATH,
97
98
  junie: JUNIE_SKILLS_PATH,
98
99
  cursor: CURSOR_SKILLS_PATH,
100
+ windsurf: WINDSURF_SKILLS_PATH,
99
101
  factory: FACTORY_SKILLS_PATH,
100
102
  antigravity: ANTIGRAVITY_SKILLS_PATH,
101
103
  };
@@ -130,6 +132,7 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
130
132
  ['gemini', ['gemini-cli']],
131
133
  ['junie', ['junie']],
132
134
  ['cursor', ['cursor']],
135
+ ['windsurf', ['windsurf']],
133
136
  ['factory', ['factory']],
134
137
  ['antigravity', ['antigravity']],
135
138
  ]);
@@ -160,6 +163,7 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
160
163
  const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH);
161
164
  const junieSkillsPath = path.join(projectRoot, constants_1.JUNIE_SKILLS_PATH);
162
165
  const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH);
166
+ const windsurfSkillsPath = path.join(projectRoot, constants_1.WINDSURF_SKILLS_PATH);
163
167
  const factorySkillsPath = path.join(projectRoot, constants_1.FACTORY_SKILLS_PATH);
164
168
  const antigravitySkillsPath = path.join(projectRoot, constants_1.ANTIGRAVITY_SKILLS_PATH);
165
169
  // Clean up .claude/skills
@@ -302,6 +306,20 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
302
306
  catch {
303
307
  // Directory doesn't exist, nothing to clean
304
308
  }
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
+ }
319
+ }
320
+ catch {
321
+ // Directory doesn't exist, nothing to clean
322
+ }
305
323
  // Clean up .factory/skills
306
324
  try {
307
325
  await fs.access(factorySkillsPath);
@@ -421,6 +439,10 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
421
439
  (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CURSOR_SKILLS_PATH} for Cursor`, verbose, dryRun);
422
440
  await propagateSkillsForCursor(projectRoot, { dryRun });
423
441
  }
442
+ if (selectedTargets.has('windsurf')) {
443
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.WINDSURF_SKILLS_PATH} for Windsurf`, verbose, dryRun);
444
+ await propagateSkillsForWindsurf(projectRoot, { dryRun });
445
+ }
424
446
  if (selectedTargets.has('factory')) {
425
447
  (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.FACTORY_SKILLS_PATH} for Factory Droid`, verbose, dryRun);
426
448
  await propagateSkillsForFactory(projectRoot, { dryRun });
@@ -922,6 +944,56 @@ async function propagateSkillsForCursor(projectRoot, options) {
922
944
  }
923
945
  return [];
924
946
  }
947
+ /**
948
+ * Propagates skills for Windsurf by copying .ruler/skills to .windsurf/skills.
949
+ * Uses atomic replace to ensure safe overwriting of existing skills.
950
+ * Returns dry-run steps if dryRun is true, otherwise returns empty array.
951
+ */
952
+ async function propagateSkillsForWindsurf(projectRoot, options) {
953
+ const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
954
+ const windsurfSkillsPath = path.join(projectRoot, constants_1.WINDSURF_SKILLS_PATH);
955
+ const windsurfDir = path.dirname(windsurfSkillsPath);
956
+ // Check if source skills directory exists
957
+ try {
958
+ await fs.access(skillsDir);
959
+ }
960
+ catch {
961
+ // No skills directory - return empty
962
+ return [];
963
+ }
964
+ if (options.dryRun) {
965
+ return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.WINDSURF_SKILLS_PATH}`];
966
+ }
967
+ // Ensure .windsurf directory exists
968
+ await fs.mkdir(windsurfDir, { recursive: true });
969
+ // Use atomic replace: copy to temp, then rename
970
+ const tempDir = path.join(windsurfDir, `skills.tmp-${Date.now()}`);
971
+ try {
972
+ // Copy to temp directory
973
+ await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
974
+ // Atomically replace the target
975
+ // First, remove existing target if it exists
976
+ try {
977
+ await fs.rm(windsurfSkillsPath, { recursive: true, force: true });
978
+ }
979
+ catch {
980
+ // Target didn't exist, that's fine
981
+ }
982
+ // Rename temp to target
983
+ await fs.rename(tempDir, windsurfSkillsPath);
984
+ }
985
+ catch (error) {
986
+ // Clean up temp directory on error
987
+ try {
988
+ await fs.rm(tempDir, { recursive: true, force: true });
989
+ }
990
+ catch {
991
+ // Ignore cleanup errors
992
+ }
993
+ throw error;
994
+ }
995
+ return [];
996
+ }
925
997
  /**
926
998
  * Propagates skills for Factory Droid by copying .ruler/skills to .factory/skills.
927
999
  * Uses atomic replace to ensure safe overwriting of existing skills.
@@ -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
+ }
package/dist/lib.js CHANGED
@@ -53,6 +53,17 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
53
53
  ? configSetting
54
54
  : true; // default to enabled
55
55
  }
56
+ /**
57
+ * Resolves subagents enabled state based on precedence:
58
+ * CLI flag > ruler.toml > default (enabled).
59
+ */
60
+ function resolveSubagentsEnabled(cliFlag, configSetting) {
61
+ return cliFlag !== undefined
62
+ ? cliFlag
63
+ : configSetting !== undefined
64
+ ? configSetting
65
+ : true; // default to enabled
66
+ }
56
67
  /**
57
68
  * Applies ruler configurations for all supported AI agents.
58
69
  * @param projectRoot Root directory of the project
@@ -62,7 +73,7 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
62
73
  * @param projectRoot Root directory of the project
63
74
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
64
75
  */
65
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal) {
76
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
66
77
  // Load configuration and rules
67
78
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
68
79
  if (configPath) {
@@ -100,6 +111,16 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
100
111
  await propagateSkills(nestedRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
101
112
  }
102
113
  }
114
+ // Propagate subagents (mirrors skills handling for nested mode).
115
+ const subagentsEnabledResolved = resolveSubagentsEnabled(subagentsEnabled, rootConfig.subagents?.enabled);
116
+ {
117
+ const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
118
+ for (const configEntry of hierarchicalConfigs) {
119
+ const nestedRoot = path.dirname(configEntry.rulerDir);
120
+ (0, constants_1.logVerbose)(`Propagating subagents for nested directory: ${nestedRoot}`, verbose);
121
+ await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, verbose, dryRun);
122
+ }
123
+ }
103
124
  generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
104
125
  }
105
126
  else {
@@ -117,6 +138,12 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
117
138
  const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
118
139
  await propagateSkills(projectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
119
140
  }
141
+ // Propagate subagents (mirrors skills handling).
142
+ const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
143
+ {
144
+ const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
145
+ await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, verbose, dryRun);
146
+ }
120
147
  generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
121
148
  }
122
149
  // Add skills-generated paths to gitignore if skills are enabled
@@ -126,7 +153,14 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
126
153
  // Skills enabled by default or explicitly
127
154
  const { getSkillsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
128
155
  const skillsPaths = await getSkillsGitignorePaths(projectRoot, selectedAgents);
129
- allGeneratedPaths = [...generatedPaths, ...skillsPaths];
156
+ allGeneratedPaths = [...allGeneratedPaths, ...skillsPaths];
157
+ }
158
+ // Add subagents-generated paths to gitignore if subagents are enabled.
159
+ const subagentsEnabledForGitignore = resolveSubagentsEnabled(subagentsEnabled, loadedConfig.subagents?.enabled);
160
+ if (subagentsEnabledForGitignore) {
161
+ const { getSubagentsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
162
+ const subagentPaths = await getSubagentsGitignorePaths(projectRoot, selectedAgents);
163
+ allGeneratedPaths = [...allGeneratedPaths, ...subagentPaths];
130
164
  }
131
165
  await (0, apply_engine_1.updateGitignore)(projectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun, cliGitignoreLocal);
132
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.37",
3
+ "version": "0.3.39",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {