@intellectronica/ruler 0.3.38 → 0.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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` | 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. |
@@ -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,153 @@ 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
+ Subagent propagation is **disabled by default**. Opt in via CLI flag or `ruler.toml`:
794
+
795
+ ```bash
796
+ ruler apply --subagents # enable subagent propagation for one run
797
+ ```
798
+
799
+ ```toml
800
+ # .ruler/ruler.toml
801
+ [agents]
802
+ enabled = true
803
+ # include_in_rules = true # also append .ruler/agents/*.md into top-level CLAUDE.md / AGENTS.md (default: false)
804
+ ```
805
+
806
+ > **Note:** the previous release used `[subagents]` for these keys. `[subagents]` is still honored as a fallback with a deprecation warning, and will be removed in a future release. Please migrate to `[agents]`.
807
+
808
+ `[agents] enabled` controls only native subagent propagation from `.ruler/agents/`. It is independent from `[agents.<name>] enabled` (which toggles per-coding-agent output like `CLAUDE.md` / `AGENTS.md`).
809
+
810
+ CLI flags take precedence over `ruler.toml`, which takes precedence over the default (disabled).
811
+
812
+ ### Validation
813
+
814
+ Source files are validated at discovery time:
815
+
816
+ - Files without YAML frontmatter are skipped with a warning.
817
+ - Files missing required `name` or `description` are skipped with a warning.
818
+ - Files where `name` does not match the filename stem are skipped with a warning.
819
+ - Unknown frontmatter keys are dropped (not errored).
820
+
821
+ ### Dry-Run Mode
822
+
823
+ Use `--dry-run` to preview which files would be written without touching disk.
824
+
825
+ ### `.gitignore` Integration
826
+
827
+ When subagents are enabled, the four target directories are added to the Ruler-managed block of `.gitignore`:
828
+
829
+ ```
830
+ .claude/agents/
831
+ .cursor/agents/
832
+ .codex/agents/
833
+ .github/agents/
834
+ ```
835
+
836
+ Use `--no-gitignore` to opt out.
837
+
838
+ ### Cleanup
839
+
840
+ Subagent propagation does **not** currently have explicit `ruler revert` support. To remove generated subagent directories, set `[agents] 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.
841
+
842
+ ### Example Workflow
843
+
844
+ ```bash
845
+ # 1. Author a subagent in your project
846
+ mkdir -p .ruler/agents
847
+ cat > .ruler/agents/code-reviewer.md << 'EOF'
848
+ ---
849
+ name: code-reviewer
850
+ description: Reviews changes against SOLID/DRY/KISS
851
+ tools: [Read, Grep, Glob]
852
+ readonly: true
853
+ ---
854
+
855
+ You review code changes for quality.
856
+ EOF
857
+
858
+ # 2. Opt subagents in (default is disabled — see [agents] section above)
859
+ echo -e "\n[agents]\nenabled = true" >> .ruler/ruler.toml
860
+
861
+ # 3. Apply
862
+ ruler apply
863
+
864
+ # 4. The subagent is now available in each agent's native location:
865
+ # - Claude Code: .claude/agents/code-reviewer.md
866
+ # - Cursor: .cursor/agents/code-reviewer.md
867
+ # - Codex CLI: .codex/agents/code-reviewer.toml
868
+ # - GitHub Copilot: .github/agents/code-reviewer.md
869
+ ```
870
+
871
+ ### Limitations
872
+
873
+ - **No explicit revert command.** Cleanup happens via `[agents] enabled = false` on a subsequent `apply`.
874
+ - **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.
875
+ - **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.
876
+
728
877
  ## `.gitignore` Integration
729
878
 
730
879
  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;
@@ -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.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';
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._resetLegacySubagentsWarningForTests = _resetLegacySubagentsWarningForTests;
36
37
  exports.loadConfig = loadConfig;
37
38
  const fs_1 = require("fs");
38
39
  const path = __importStar(require("path"));
@@ -40,6 +41,21 @@ const os = __importStar(require("os"));
40
41
  const toml_1 = require("@iarna/toml");
41
42
  const zod_1 = require("zod");
42
43
  const constants_1 = require("../constants");
44
+ // One-shot guard so the deprecation message fires once per process even when
45
+ // `loadConfig` is called multiple times (e.g. nested mode walks every
46
+ // `.ruler` directory).
47
+ let _legacySubagentsWarned = false;
48
+ function warnLegacySubagentsSection() {
49
+ if (_legacySubagentsWarned)
50
+ return;
51
+ _legacySubagentsWarned = true;
52
+ (0, constants_1.logWarn)('`[subagents]` is deprecated; rename it to `[agents]` in your ruler.toml. ' +
53
+ 'The legacy section is honored for now and will be removed in a future release.');
54
+ }
55
+ /** Test helper — re-arms the deprecation guard so suites can assert it fires. */
56
+ function _resetLegacySubagentsWarningForTests() {
57
+ _legacySubagentsWarned = false;
58
+ }
43
59
  const mcpConfigSchema = zod_1.z
44
60
  .object({
45
61
  enabled: zod_1.z.boolean().optional(),
@@ -55,9 +71,21 @@ const agentConfigSchema = zod_1.z
55
71
  mcp: mcpConfigSchema,
56
72
  })
57
73
  .optional();
74
+ // `[agents]` is a heterogeneous table that holds two unrelated kinds of keys:
75
+ // - reserved subagent-control booleans (`enabled`, `include_in_rules`)
76
+ // - one nested table per coding-agent integration (`[agents.claude]`, etc.)
77
+ // Reserved keys are validated by the object shape; everything else falls
78
+ // through `catchall` and is treated as a per-agent config record.
79
+ const SUBAGENT_RESERVED_KEYS = new Set(['enabled', 'include_in_rules']);
58
80
  const rulerConfigSchema = zod_1.z.object({
59
81
  default_agents: zod_1.z.array(zod_1.z.string()).optional(),
60
- agents: zod_1.z.record(zod_1.z.string(), agentConfigSchema).optional(),
82
+ agents: zod_1.z
83
+ .object({
84
+ enabled: zod_1.z.boolean().optional(),
85
+ include_in_rules: zod_1.z.boolean().optional(),
86
+ })
87
+ .catchall(agentConfigSchema)
88
+ .optional(),
61
89
  mcp: zod_1.z
62
90
  .object({
63
91
  enabled: zod_1.z.boolean().optional(),
@@ -75,6 +103,16 @@ const rulerConfigSchema = zod_1.z.object({
75
103
  enabled: zod_1.z.boolean().optional(),
76
104
  })
77
105
  .optional(),
106
+ // Deprecated: kept in the schema only so that legacy `[subagents]` blocks
107
+ // are preserved through validation. The parser reads from here as a
108
+ // fallback when the new `[agents]` keys are absent and emits a one-time
109
+ // deprecation warning. Remove in the next minor release.
110
+ subagents: zod_1.z
111
+ .object({
112
+ enabled: zod_1.z.boolean().optional(),
113
+ include_in_rules: zod_1.z.boolean().optional(),
114
+ })
115
+ .optional(),
78
116
  nested: zod_1.z.boolean().optional(),
79
117
  });
80
118
  /**
@@ -150,6 +188,11 @@ async function loadConfig(options) {
150
188
  : {};
151
189
  const agentConfigs = {};
152
190
  for (const [name, section] of Object.entries(agentsSection)) {
191
+ // Reserved subagent-control keys live alongside per-agent records in
192
+ // the same `[agents]` table; skip them here so we only process actual
193
+ // coding-agent integrations as agent configs.
194
+ if (SUBAGENT_RESERVED_KEYS.has(name))
195
+ continue;
153
196
  if (section && typeof section === 'object') {
154
197
  const sectionObj = section;
155
198
  const cfg = {};
@@ -214,6 +257,40 @@ async function loadConfig(options) {
214
257
  if (typeof rawSkillsSection.enabled === 'boolean') {
215
258
  skillsConfig.enabled = rawSkillsSection.enabled;
216
259
  }
260
+ // Subagent control lives under `[agents]` (alongside per-agent records).
261
+ // The reserved keys `enabled` and `include_in_rules` are pulled out here
262
+ // and surfaced internally as `LoadedConfig.subagents` for the rest of the
263
+ // codebase, which still uses the `Subagent*` naming.
264
+ //
265
+ // Backward-compatibility: the previous release used `[subagents]` for the
266
+ // same two keys. We still read those as a fallback when the matching
267
+ // `[agents]` key is absent, and emit a one-time deprecation warning so
268
+ // existing configs keep working while users migrate.
269
+ const rawLegacySubagentsSection = raw.subagents &&
270
+ typeof raw.subagents === 'object' &&
271
+ !Array.isArray(raw.subagents)
272
+ ? raw.subagents
273
+ : {};
274
+ const legacyHasContent = typeof rawLegacySubagentsSection.enabled === 'boolean' ||
275
+ typeof rawLegacySubagentsSection.include_in_rules === 'boolean';
276
+ if (legacyHasContent) {
277
+ warnLegacySubagentsSection();
278
+ }
279
+ const subagentsConfig = {};
280
+ if (typeof agentsSection.enabled === 'boolean') {
281
+ subagentsConfig.enabled = agentsSection.enabled;
282
+ }
283
+ else if (typeof rawLegacySubagentsSection.enabled === 'boolean') {
284
+ subagentsConfig.enabled = rawLegacySubagentsSection.enabled;
285
+ }
286
+ if (typeof agentsSection.include_in_rules === 'boolean') {
287
+ subagentsConfig.include_in_rules =
288
+ agentsSection.include_in_rules;
289
+ }
290
+ else if (typeof rawLegacySubagentsSection.include_in_rules === 'boolean') {
291
+ subagentsConfig.include_in_rules =
292
+ rawLegacySubagentsSection.include_in_rules;
293
+ }
217
294
  const nestedDefined = typeof raw.nested === 'boolean';
218
295
  const nested = nestedDefined ? raw.nested : false;
219
296
  return {
@@ -223,6 +300,7 @@ async function loadConfig(options) {
223
300
  mcp: globalMcpConfig,
224
301
  gitignore: gitignoreConfig,
225
302
  skills: skillsConfig,
303
+ subagents: subagentsConfig,
226
304
  nested,
227
305
  nestedDefined,
228
306
  };
@@ -44,6 +44,7 @@ const fs_1 = require("fs");
44
44
  const path = __importStar(require("path"));
45
45
  const os = __importStar(require("os"));
46
46
  const constants_1 = require("../constants");
47
+ const SUBAGENTS_DIR_NAME = path.basename(constants_1.RULER_SUBAGENTS_PATH);
47
48
  /**
48
49
  * Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
49
50
  */
@@ -93,9 +94,18 @@ async function findRulerDir(startPath, checkGlobal = true) {
93
94
  /**
94
95
  * Recursively reads all Markdown (.md) files in rulerDir, returning their paths and contents.
95
96
  * Files are sorted alphabetically by path.
97
+ *
98
+ * `.ruler/skills/` is always skipped (skills are propagated separately).
99
+ * `.ruler/agents/` is skipped unless `options.includeAgents` is `true`.
96
100
  */
97
- async function readMarkdownFiles(rulerDir) {
101
+ async function readMarkdownFiles(rulerDir, options = {}) {
98
102
  const mdFiles = [];
103
+ const includeAgents = options.includeAgents === true;
104
+ // Tracks whether we skipped a `.ruler/agents` subtree so the root-AGENTS.md
105
+ // fallback below still recognises ruler content as present and does not
106
+ // resurrect a previously generated root AGENTS.md (which may itself contain
107
+ // the very agent docs we're now excluding).
108
+ let sawExcludedAgents = false;
99
109
  // Gather all markdown files (recursive) first
100
110
  async function walk(dir) {
101
111
  const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
@@ -115,13 +125,22 @@ async function readMarkdownFiles(rulerDir) {
115
125
  }
116
126
  }
117
127
  if (isDir) {
118
- // Skip .ruler/skills; skills are propagated separately and should not be concatenated
119
128
  const relativeFromRoot = path.relative(rulerDir, fullPath);
129
+ // Skip .ruler/skills; skills are propagated separately and should not be concatenated
120
130
  const isSkillsDir = relativeFromRoot === constants_1.SKILLS_DIR ||
121
131
  relativeFromRoot.startsWith(`${constants_1.SKILLS_DIR}${path.sep}`);
122
132
  if (isSkillsDir) {
123
133
  continue;
124
134
  }
135
+ // Skip .ruler/agents unless explicitly opted in via subagents.include_in_rules.
136
+ // Subagents are propagated separately to native locations and should not pollute
137
+ // the top-level rule concatenation by default.
138
+ const isAgentsDir = relativeFromRoot === SUBAGENTS_DIR_NAME ||
139
+ relativeFromRoot.startsWith(`${SUBAGENTS_DIR_NAME}${path.sep}`);
140
+ if (isAgentsDir && !includeAgents) {
141
+ sawExcludedAgents = true;
142
+ continue;
143
+ }
125
144
  await walk(fullPath);
126
145
  }
127
146
  else if (isFile && entry.name.endsWith('.md')) {
@@ -170,9 +189,12 @@ async function readMarkdownFiles(rulerDir) {
170
189
  const stat = await fs_1.promises.stat(rootAgentsPath);
171
190
  if (stat.isFile()) {
172
191
  const content = await fs_1.promises.readFile(rootAgentsPath, 'utf8');
173
- // Check if this is a generated file and we have other .ruler files
192
+ // Check if this is a generated file and we have other .ruler files.
193
+ // `sawExcludedAgents` counts as "ruler content present" so a stale
194
+ // generated root AGENTS.md isn't resurrected when `.ruler/agents` was
195
+ // the only source under `.ruler` and is now being skipped.
174
196
  const isGenerated = content.startsWith('<!-- Generated by Ruler -->');
175
- const hasRulerFiles = others.length > 0 || primaryFile !== null;
197
+ const hasRulerFiles = others.length > 0 || primaryFile !== null || sawExcludedAgents;
176
198
  // Additional check: if AGENTS.md contains ruler source comments and we have ruler files,
177
199
  // it's likely a corrupted generated file that should be skipped
178
200
  const containsRulerSources = content.includes('<!-- Source: .ruler/') ||