@intellectronica/ruler 0.3.39 → 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 +17 -9
- package/dist/core/ConfigLoader.js +67 -4
- package/dist/core/FileSystemUtils.js +26 -4
- package/dist/core/apply-engine.js +17 -14
- package/dist/lib.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -790,19 +790,24 @@ For GitHub Copilot, source `tools` (Claude vocabulary: `Read`, `Grep`, `Bash`,
|
|
|
790
790
|
|
|
791
791
|
### Configuration
|
|
792
792
|
|
|
793
|
-
|
|
793
|
+
Subagent propagation is **disabled by default**. Opt in via CLI flag or `ruler.toml`:
|
|
794
794
|
|
|
795
795
|
```bash
|
|
796
|
-
ruler apply --
|
|
796
|
+
ruler apply --subagents # enable subagent propagation for one run
|
|
797
797
|
```
|
|
798
798
|
|
|
799
799
|
```toml
|
|
800
800
|
# .ruler/ruler.toml
|
|
801
|
-
[
|
|
802
|
-
enabled =
|
|
801
|
+
[agents]
|
|
802
|
+
enabled = true
|
|
803
|
+
# include_in_rules = true # also append .ruler/agents/*.md into top-level CLAUDE.md / AGENTS.md (default: false)
|
|
803
804
|
```
|
|
804
805
|
|
|
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).
|
|
806
811
|
|
|
807
812
|
### Validation
|
|
808
813
|
|
|
@@ -832,7 +837,7 @@ Use `--no-gitignore` to opt out.
|
|
|
832
837
|
|
|
833
838
|
### Cleanup
|
|
834
839
|
|
|
835
|
-
Subagent propagation does **not** currently have explicit `ruler revert` support. To remove generated subagent directories, set `[
|
|
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.
|
|
836
841
|
|
|
837
842
|
### Example Workflow
|
|
838
843
|
|
|
@@ -850,10 +855,13 @@ readonly: true
|
|
|
850
855
|
You review code changes for quality.
|
|
851
856
|
EOF
|
|
852
857
|
|
|
853
|
-
# 2.
|
|
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
|
|
854
862
|
ruler apply
|
|
855
863
|
|
|
856
|
-
#
|
|
864
|
+
# 4. The subagent is now available in each agent's native location:
|
|
857
865
|
# - Claude Code: .claude/agents/code-reviewer.md
|
|
858
866
|
# - Cursor: .cursor/agents/code-reviewer.md
|
|
859
867
|
# - Codex CLI: .codex/agents/code-reviewer.toml
|
|
@@ -862,7 +870,7 @@ ruler apply
|
|
|
862
870
|
|
|
863
871
|
### Limitations
|
|
864
872
|
|
|
865
|
-
- **No explicit revert command.** Cleanup happens via `[
|
|
873
|
+
- **No explicit revert command.** Cleanup happens via `[agents] enabled = false` on a subsequent `apply`.
|
|
866
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.
|
|
867
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.
|
|
868
876
|
|
|
@@ -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
|
|
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,9 +103,14 @@ 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.
|
|
78
110
|
subagents: zod_1.z
|
|
79
111
|
.object({
|
|
80
112
|
enabled: zod_1.z.boolean().optional(),
|
|
113
|
+
include_in_rules: zod_1.z.boolean().optional(),
|
|
81
114
|
})
|
|
82
115
|
.optional(),
|
|
83
116
|
nested: zod_1.z.boolean().optional(),
|
|
@@ -155,6 +188,11 @@ async function loadConfig(options) {
|
|
|
155
188
|
: {};
|
|
156
189
|
const agentConfigs = {};
|
|
157
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;
|
|
158
196
|
if (section && typeof section === 'object') {
|
|
159
197
|
const sectionObj = section;
|
|
160
198
|
const cfg = {};
|
|
@@ -219,14 +257,39 @@ async function loadConfig(options) {
|
|
|
219
257
|
if (typeof rawSkillsSection.enabled === 'boolean') {
|
|
220
258
|
skillsConfig.enabled = rawSkillsSection.enabled;
|
|
221
259
|
}
|
|
222
|
-
|
|
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 &&
|
|
223
270
|
typeof raw.subagents === 'object' &&
|
|
224
271
|
!Array.isArray(raw.subagents)
|
|
225
272
|
? raw.subagents
|
|
226
273
|
: {};
|
|
274
|
+
const legacyHasContent = typeof rawLegacySubagentsSection.enabled === 'boolean' ||
|
|
275
|
+
typeof rawLegacySubagentsSection.include_in_rules === 'boolean';
|
|
276
|
+
if (legacyHasContent) {
|
|
277
|
+
warnLegacySubagentsSection();
|
|
278
|
+
}
|
|
227
279
|
const subagentsConfig = {};
|
|
228
|
-
if (typeof
|
|
229
|
-
subagentsConfig.enabled =
|
|
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;
|
|
230
293
|
}
|
|
231
294
|
const nestedDefined = typeof raw.nested === 'boolean';
|
|
232
295
|
const nested = nestedDefined ? raw.nested : false;
|
|
@@ -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/') ||
|
|
@@ -56,25 +56,23 @@ const constants_1 = require("../constants");
|
|
|
56
56
|
async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
|
|
57
57
|
const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
|
|
58
58
|
const results = [];
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Load config first so we know whether `.ruler/agents/` should be included
|
|
60
|
+
// in the rule concatenation for each directory.
|
|
61
|
+
for (const rulerDir of rulerDirs) {
|
|
61
62
|
const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
|
|
63
|
+
const files = await FileSystemUtils.readMarkdownFiles(rulerDir, {
|
|
64
|
+
includeAgents: shouldIncludeAgentsInRules(config),
|
|
65
|
+
});
|
|
62
66
|
results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
|
|
63
67
|
}
|
|
64
68
|
return results;
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
71
|
+
* Returns true when `.ruler/agents/*.md` should be concatenated into the
|
|
72
|
+
* generated top-level rule files. Defaults to false.
|
|
69
73
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Process each .ruler directory independently
|
|
73
|
-
for (const rulerDir of rulerDirs) {
|
|
74
|
-
const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
|
|
75
|
-
results.push({ rulerDir, files });
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
74
|
+
function shouldIncludeAgentsInRules(config) {
|
|
75
|
+
return config.subagents?.include_in_rules === true;
|
|
78
76
|
}
|
|
79
77
|
async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
|
|
80
78
|
await warnAboutLegacyMcpJson(rulerDir);
|
|
@@ -146,6 +144,8 @@ function cloneLoadedConfig(config) {
|
|
|
146
144
|
cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
|
|
147
145
|
mcp: config.mcp ? { ...config.mcp } : undefined,
|
|
148
146
|
gitignore: config.gitignore ? { ...config.gitignore } : undefined,
|
|
147
|
+
skills: config.skills ? { ...config.skills } : undefined,
|
|
148
|
+
subagents: config.subagents ? { ...config.subagents } : undefined,
|
|
149
149
|
nested: config.nested,
|
|
150
150
|
nestedDefined: config.nestedDefined,
|
|
151
151
|
};
|
|
@@ -203,8 +203,11 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
|
|
|
203
203
|
projectRoot,
|
|
204
204
|
configPath,
|
|
205
205
|
});
|
|
206
|
-
// Read rule files
|
|
207
|
-
|
|
206
|
+
// Read rule files. `.ruler/agents/` is only included when
|
|
207
|
+
// `[agents] include_in_rules = true`.
|
|
208
|
+
const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0], {
|
|
209
|
+
includeAgents: shouldIncludeAgentsInRules(config),
|
|
210
|
+
});
|
|
208
211
|
// Concatenate rules
|
|
209
212
|
const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(primaryDir));
|
|
210
213
|
// Load unified config to get merged MCP configuration
|
package/dist/lib.js
CHANGED
|
@@ -55,14 +55,20 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
|
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Resolves subagents enabled state based on precedence:
|
|
58
|
-
* CLI flag > ruler.toml > default (
|
|
58
|
+
* CLI flag > ruler.toml > default (disabled).
|
|
59
|
+
*
|
|
60
|
+
* When neither `[agents] enabled` (nor the legacy `[subagents] enabled`)
|
|
61
|
+
* nor a CLI flag is provided, propagation is disabled by default per spec.
|
|
62
|
+
* Subagent definitions are an opt-in feature — propagating them silently
|
|
63
|
+
* could leak runtime prompts into native subagent locations on projects
|
|
64
|
+
* that never intended to use the feature.
|
|
59
65
|
*/
|
|
60
66
|
function resolveSubagentsEnabled(cliFlag, configSetting) {
|
|
61
67
|
return cliFlag !== undefined
|
|
62
68
|
? cliFlag
|
|
63
69
|
: configSetting !== undefined
|
|
64
70
|
? configSetting
|
|
65
|
-
:
|
|
71
|
+
: false; // default to disabled — see spec: subagents must opt in
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
68
74
|
* Applies ruler configurations for all supported AI agents.
|