@intellectronica/ruler 0.3.38 → 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 +177 -36
- package/dist/agents/ClaudeAgent.js +3 -0
- package/dist/agents/CodexCliAgent.js +3 -0
- package/dist/agents/CopilotAgent.js +3 -0
- package/dist/agents/CursorAgent.js +3 -0
- package/dist/cli/commands.js +4 -0
- package/dist/cli/handlers.js +9 -1
- package/dist/constants.js +7 -1
- package/dist/core/ConfigLoader.js +15 -0
- package/dist/core/SubagentsProcessor.js +440 -0
- package/dist/core/SubagentsUtils.js +195 -0
- package/dist/lib.js +36 -2
- package/package.json +1 -1
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` | `.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` | - | - |
|
|
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` |
|
|
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. |
|
|
@@ -663,12 +666,13 @@ When skills support is enabled and gitignore integration is active, Ruler automa
|
|
|
663
666
|
- `.gemini/skills/` (for Gemini CLI)
|
|
664
667
|
- `.junie/skills/` (for Junie)
|
|
665
668
|
- `.cursor/skills/` (for Cursor)
|
|
669
|
+
- `.windsurf/skills/` (for Windsurf)
|
|
666
670
|
|
|
667
671
|
to your `.gitignore` file within the managed Ruler block.
|
|
668
672
|
|
|
669
673
|
### Requirements
|
|
670
674
|
|
|
671
|
-
- **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.
|
|
672
676
|
|
|
673
677
|
### Validation
|
|
674
678
|
|
|
@@ -723,8 +727,145 @@ ruler apply
|
|
|
723
727
|
# - Gemini CLI: .gemini/skills/my-skill/
|
|
724
728
|
# - Junie: .junie/skills/my-skill/
|
|
725
729
|
# - Cursor: .cursor/skills/my-skill/
|
|
730
|
+
# - Windsurf: .windsurf/skills/my-skill/
|
|
726
731
|
```
|
|
727
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
|
+
|
|
728
869
|
## `.gitignore` Integration
|
|
729
870
|
|
|
730
871
|
Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
|
package/dist/cli/commands.js
CHANGED
|
@@ -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) => {
|
package/dist/cli/handlers.js
CHANGED
|
@@ -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.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;
|
|
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;
|
|
@@ -66,3 +66,9 @@ exports.WINDSURF_SKILLS_PATH = '.windsurf/skills';
|
|
|
66
66
|
exports.FACTORY_SKILLS_PATH = '.factory/skills';
|
|
67
67
|
exports.ANTIGRAVITY_SKILLS_PATH = '.agent/skills';
|
|
68
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
|
};
|
|
@@ -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 = [...
|
|
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
|
}
|