gitea push

This commit is contained in:
2026-05-09 12:19:29 -06:00
parent 06113c95b8
commit 429461e985
1481 changed files with 74306 additions and 52475 deletions
+185 -36
View File
@@ -54,40 +54,40 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
## Supported AI Agents
| Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location |
| ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- |
| AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - |
| GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` |
| Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` |
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.codex/skills/` |
| Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` |
| Jules | `AGENTS.md` | - | - |
| Cursor | `AGENTS.md` | `.cursor/mcp.json` | `.cursor/skills/` |
| Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` | `.windsurf/skills/` |
| Cline | `.clinerules` | - | - |
| Crush | `CRUSH.md` | `.crush.json` | - |
| Amp | `AGENTS.md` | - | `.agents/skills/` |
| Antigravity | `.agent/rules/ruler.md` | - | `.agent/skills/` |
| Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` | - |
| Aider | `AGENTS.md`, `.aider.conf.yml` | `.mcp.json` | - |
| Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - |
| Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - |
| Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` |
| Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | `.junie/skills/` |
| AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - |
| Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` |
| OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` |
| Goose | `.goosehints` | - | `.agents/skills/` |
| Qwen Code | `AGENTS.md` | `.qwen/settings.json` | - |
| RooCode | `AGENTS.md` | `.roo/mcp.json` | `.roo/skills/` |
| Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) | - |
| Trae AI | `.trae/rules/project_rules.md` | - | - |
| Warp | `WARP.md` | - | - |
| Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` | - |
| Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) | - |
| Factory Droid | `AGENTS.md` | `.factory/mcp.json` | `.factory/skills/` |
| Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` | `.vibe/skills/` |
| JetBrains AI Assistant | `.aiassistant/rules/AGENTS.md` | - | - |
| Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location | Subagents Support / Location |
| ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- | ---------------------------- |
| AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - | - |
| GitHub Copilot | `AGENTS.md` | `.vscode/mcp.json` | `.claude/skills/` | `.github/agents/` |
| Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` | `.claude/agents/` |
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.codex/skills/` | `.codex/agents/` (`.toml`) |
| Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` | - |
| Jules | `AGENTS.md` | - | - | - |
| Cursor | `AGENTS.md` | `.cursor/mcp.json` | `.cursor/skills/` | `.cursor/agents/` |
| Windsurf | `AGENTS.md` | `.windsurf/mcp_config.json` | `.windsurf/skills/` | - |
| Cline | `.clinerules` | - | - | - |
| Crush | `CRUSH.md` | `.crush.json` | - | - |
| Amp | `AGENTS.md` | - | `.agents/skills/` | - |
| Antigravity | `.agent/rules/ruler.md` | - | `.agent/skills/` | - |
| Amazon Q CLI | `.amazonq/rules/ruler_q_rules.md` | `.amazonq/mcp.json` | - | - |
| Aider | `AGENTS.md`, `.aider.conf.yml` | `.mcp.json` | - | - |
| Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - | - |
| Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - | - |
| Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` | - |
| Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | `.junie/skills/` | - |
| AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - | - |
| Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` | - |
| OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` | - |
| Goose | `.goosehints` | - | `.agents/skills/` | - |
| Qwen Code | `AGENTS.md` | `.qwen/settings.json` | - | - |
| RooCode | `AGENTS.md` | `.roo/mcp.json` | `.roo/skills/` | - |
| Zed | `AGENTS.md` | `.zed/settings.json` (project root, never $HOME) | - | - |
| Trae AI | `.trae/rules/project_rules.md` | - | - | - |
| Warp | `WARP.md` | - | - | - |
| Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` | - | - |
| Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) | - | - |
| Factory Droid | `AGENTS.md` | `.factory/mcp.json` | `.factory/skills/` | - |
| Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` | `.vibe/skills/` | - |
| JetBrains AI Assistant | `.aiassistant/rules/AGENTS.md` | - | - | - |
## Getting Started
@@ -241,9 +241,12 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
| `--gitignore-local` | Write managed ignore entries to `.git/info/exclude` instead. |
| `--nested` | Enable nested rule loading (default: inherit from config or disabled). |
| `--no-nested` | Disable nested rule loading even if `nested = true` in config. |
| `--backup` | Toggle creation of `.bak` backup files (default: enabled). |
| `--backup` | Enable creation of `.bak` backup files (default: enabled). |
| `--no-backup` | Disable creation of `.bak` backup files. |
| `--skills` | Enable skills support (experimental, default: enabled). |
| `--no-skills` | Disable skills support. |
| `--subagents` | Enable subagents support (experimental, default: enabled). |
| `--no-subagents` | Disable subagents support. |
| `--dry-run` | Preview changes without writing files. |
| `--local-only` | Skip `$XDG_CONFIG_HOME` when looking for configuration. |
| `--verbose` / `-v` | Display detailed output during execution. |
@@ -663,12 +666,13 @@ When skills support is enabled and gitignore integration is active, Ruler automa
- `.gemini/skills/` (for Gemini CLI)
- `.junie/skills/` (for Junie)
- `.cursor/skills/` (for Cursor)
- `.windsurf/skills/` (for Windsurf)
to your `.gitignore` file within the managed Ruler block.
### Requirements
- **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.
- **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.
### Validation
@@ -723,8 +727,153 @@ ruler apply
# - Gemini CLI: .gemini/skills/my-skill/
# - Junie: .junie/skills/my-skill/
# - Cursor: .cursor/skills/my-skill/
# - Windsurf: .windsurf/skills/my-skill/
```
## Subagents Support (Experimental)
> **⚠️ Experimental:** Subagents support is experimental and behavior may change in future releases.
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.
### How It Works
For agents with a native subagent primitive, Ruler writes one file per subagent into the target directory:
| Agent | Target location | Format |
| ----------------- | ------------------------------ | ------ |
| Claude Code | `.claude/agents/<name>.md` | Markdown + YAML frontmatter |
| Cursor | `.cursor/agents/<name>.md` | Markdown + YAML frontmatter |
| OpenAI Codex CLI | `.codex/agents/<name>.toml` | TOML (one self-contained file per agent) |
| GitHub Copilot | `.github/agents/<name>.md` | Markdown + YAML frontmatter |
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.
### Source Format
Author each subagent as `.ruler/agents/<name>.md`:
```markdown
---
name: code-reviewer
description: Use PROACTIVELY after a feature/fix is implemented. Reviews against SOLID/DRY/KISS. Read-only.
tools: [Read, Grep, Glob, Bash]
model: inherit
readonly: true
is_background: false
---
# Code Reviewer
You operate in a fresh context window with read-only access. Your job is to
review the diff and surrounding code against the design principles and return
a structured verdict.
```
**Required frontmatter fields:**
| Field | Type | Notes |
| ------------- | ------ | ----------------------------------------------------- |
| `name` | string | Must match the filename stem (`code-reviewer.md``name: code-reviewer`). |
| `description` | string | When the parent agent should delegate to this subagent. |
**Optional frontmatter fields:**
| Field | Type | Used by | Default behavior |
| --------------- | ---------------- | ------------------------------------------------ | --------------------------------------------- |
| `tools` | string[] | Claude (verbatim), Copilot (mapped to aliases) | Cursor / Codex ignore; omitted if absent. |
| `model` | string | All four targets | Cursor defaults to `inherit`; others omit. |
| `readonly` | boolean | Cursor (verbatim), Codex (`sandbox_mode`), Copilot (`disable-model-invocation`) | Defaults to `false` for Cursor; omitted otherwise. |
| `is_background` | boolean | Cursor only | Defaults to `false` for Cursor. |
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.
### Configuration
Subagent propagation is **disabled by default**. Opt in via CLI flag or `ruler.toml`:
```bash
ruler apply --subagents # enable subagent propagation for one run
```
```toml
# .ruler/ruler.toml
[agents]
enabled = true
# include_in_rules = true # also append .ruler/agents/*.md into top-level CLAUDE.md / AGENTS.md (default: false)
```
> **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]`.
`[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`).
CLI flags take precedence over `ruler.toml`, which takes precedence over the default (disabled).
### Validation
Source files are validated at discovery time:
- Files without YAML frontmatter are skipped with a warning.
- Files missing required `name` or `description` are skipped with a warning.
- Files where `name` does not match the filename stem are skipped with a warning.
- Unknown frontmatter keys are dropped (not errored).
### Dry-Run Mode
Use `--dry-run` to preview which files would be written without touching disk.
### `.gitignore` Integration
When subagents are enabled, the four target directories are added to the Ruler-managed block of `.gitignore`:
```
.claude/agents/
.cursor/agents/
.codex/agents/
.github/agents/
```
Use `--no-gitignore` to opt out.
### Cleanup
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.
### Example Workflow
```bash
# 1. Author a subagent in your project
mkdir -p .ruler/agents
cat > .ruler/agents/code-reviewer.md << 'EOF'
---
name: code-reviewer
description: Reviews changes against SOLID/DRY/KISS
tools: [Read, Grep, Glob]
readonly: true
---
You review code changes for quality.
EOF
# 2. Opt subagents in (default is disabled — see [agents] section above)
echo -e "\n[agents]\nenabled = true" >> .ruler/ruler.toml
# 3. Apply
ruler apply
# 4. The subagent is now available in each agent's native location:
# - Claude Code: .claude/agents/code-reviewer.md
# - Cursor: .cursor/agents/code-reviewer.md
# - Codex CLI: .codex/agents/code-reviewer.toml
# - GitHub Copilot: .github/agents/code-reviewer.md
```
### Limitations
- **No explicit revert command.** Cleanup happens via `[agents] enabled = false` on a subsequent `apply`.
- **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.
- **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.
## `.gitignore` Integration
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 {
supportsNativeSkills() {
return true;
}
supportsNativeSubagents() {
return true;
}
}
exports.ClaudeAgent = ClaudeAgent;
@@ -149,5 +149,8 @@ class CodexCliAgent {
supportsNativeSkills() {
return true;
}
supportsNativeSubagents() {
return true;
}
}
exports.CodexCliAgent = CodexCliAgent;
@@ -42,5 +42,8 @@ class CopilotAgent {
supportsNativeSkills() {
return true;
}
supportsNativeSubagents() {
return true;
}
}
exports.CopilotAgent = CopilotAgent;
@@ -33,5 +33,8 @@ class CursorAgent extends AgentsMdAgent_1.AgentsMdAgent {
supportsNativeSkills() {
return true;
}
supportsNativeSubagents() {
return true;
}
}
exports.CursorAgent = CursorAgent;
+4
View File
@@ -77,6 +77,10 @@ function run() {
.option('skills', {
type: 'boolean',
description: 'Enable/disable skills support (experimental, default: enabled)',
})
.option('subagents', {
type: 'boolean',
description: 'Enable/disable subagents support (experimental, default: enabled)',
});
}, handlers_1.applyHandler)
.command('init', 'Scaffold a .ruler directory with default files', (y) => {
+9 -1
View File
@@ -114,8 +114,16 @@ async function applyHandler(argv) {
else {
skillsEnabled = undefined; // Let config/default decide
}
// Determine subagents preference: CLI > TOML > Default (enabled)
let subagentsEnabled;
if (argv.subagents !== undefined) {
subagentsEnabled = argv.subagents;
}
else {
subagentsEnabled = undefined; // Let config/default decide
}
try {
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup, skillsEnabled, gitignoreLocalPreference);
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup, skillsEnabled, gitignoreLocalPreference, subagentsEnabled);
console.log('Ruler apply completed successfully.');
}
catch (err) {
+7 -1
View File
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
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;
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;
exports.actionPrefix = actionPrefix;
exports.createRulerError = createRulerError;
exports.logVerbose = logVerbose;
@@ -66,3 +66,9 @@ exports.WINDSURF_SKILLS_PATH = '.windsurf/skills';
exports.FACTORY_SKILLS_PATH = '.factory/skills';
exports.ANTIGRAVITY_SKILLS_PATH = '.agent/skills';
exports.SKILL_MD_FILENAME = 'SKILL.md';
// Subagents-related constants
exports.RULER_SUBAGENTS_PATH = '.ruler/agents';
exports.CLAUDE_SUBAGENTS_PATH = '.claude/agents';
exports.CURSOR_SUBAGENTS_PATH = '.cursor/agents';
exports.CODEX_SUBAGENTS_PATH = '.codex/agents';
exports.COPILOT_SUBAGENTS_PATH = '.github/agents';
+79 -1
View File
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports._resetLegacySubagentsWarningForTests = _resetLegacySubagentsWarningForTests;
exports.loadConfig = loadConfig;
const fs_1 = require("fs");
const path = __importStar(require("path"));
@@ -40,6 +41,21 @@ const os = __importStar(require("os"));
const toml_1 = require("@iarna/toml");
const zod_1 = require("zod");
const constants_1 = require("../constants");
// One-shot guard so the deprecation message fires once per process even when
// `loadConfig` is called multiple times (e.g. nested mode walks every
// `.ruler` directory).
let _legacySubagentsWarned = false;
function warnLegacySubagentsSection() {
if (_legacySubagentsWarned)
return;
_legacySubagentsWarned = true;
(0, constants_1.logWarn)('`[subagents]` is deprecated; rename it to `[agents]` in your ruler.toml. ' +
'The legacy section is honored for now and will be removed in a future release.');
}
/** Test helper — re-arms the deprecation guard so suites can assert it fires. */
function _resetLegacySubagentsWarningForTests() {
_legacySubagentsWarned = false;
}
const mcpConfigSchema = zod_1.z
.object({
enabled: zod_1.z.boolean().optional(),
@@ -55,9 +71,21 @@ const agentConfigSchema = zod_1.z
mcp: mcpConfigSchema,
})
.optional();
// `[agents]` is a heterogeneous table that holds two unrelated kinds of keys:
// - reserved subagent-control booleans (`enabled`, `include_in_rules`)
// - one nested table per coding-agent integration (`[agents.claude]`, etc.)
// Reserved keys are validated by the object shape; everything else falls
// through `catchall` and is treated as a per-agent config record.
const SUBAGENT_RESERVED_KEYS = new Set(['enabled', 'include_in_rules']);
const rulerConfigSchema = zod_1.z.object({
default_agents: zod_1.z.array(zod_1.z.string()).optional(),
agents: zod_1.z.record(zod_1.z.string(), agentConfigSchema).optional(),
agents: zod_1.z
.object({
enabled: zod_1.z.boolean().optional(),
include_in_rules: zod_1.z.boolean().optional(),
})
.catchall(agentConfigSchema)
.optional(),
mcp: zod_1.z
.object({
enabled: zod_1.z.boolean().optional(),
@@ -75,6 +103,16 @@ const rulerConfigSchema = zod_1.z.object({
enabled: zod_1.z.boolean().optional(),
})
.optional(),
// Deprecated: kept in the schema only so that legacy `[subagents]` blocks
// are preserved through validation. The parser reads from here as a
// fallback when the new `[agents]` keys are absent and emits a one-time
// deprecation warning. Remove in the next minor release.
subagents: zod_1.z
.object({
enabled: zod_1.z.boolean().optional(),
include_in_rules: zod_1.z.boolean().optional(),
})
.optional(),
nested: zod_1.z.boolean().optional(),
});
/**
@@ -150,6 +188,11 @@ async function loadConfig(options) {
: {};
const agentConfigs = {};
for (const [name, section] of Object.entries(agentsSection)) {
// Reserved subagent-control keys live alongside per-agent records in
// the same `[agents]` table; skip them here so we only process actual
// coding-agent integrations as agent configs.
if (SUBAGENT_RESERVED_KEYS.has(name))
continue;
if (section && typeof section === 'object') {
const sectionObj = section;
const cfg = {};
@@ -214,6 +257,40 @@ async function loadConfig(options) {
if (typeof rawSkillsSection.enabled === 'boolean') {
skillsConfig.enabled = rawSkillsSection.enabled;
}
// Subagent control lives under `[agents]` (alongside per-agent records).
// The reserved keys `enabled` and `include_in_rules` are pulled out here
// and surfaced internally as `LoadedConfig.subagents` for the rest of the
// codebase, which still uses the `Subagent*` naming.
//
// Backward-compatibility: the previous release used `[subagents]` for the
// same two keys. We still read those as a fallback when the matching
// `[agents]` key is absent, and emit a one-time deprecation warning so
// existing configs keep working while users migrate.
const rawLegacySubagentsSection = raw.subagents &&
typeof raw.subagents === 'object' &&
!Array.isArray(raw.subagents)
? raw.subagents
: {};
const legacyHasContent = typeof rawLegacySubagentsSection.enabled === 'boolean' ||
typeof rawLegacySubagentsSection.include_in_rules === 'boolean';
if (legacyHasContent) {
warnLegacySubagentsSection();
}
const subagentsConfig = {};
if (typeof agentsSection.enabled === 'boolean') {
subagentsConfig.enabled = agentsSection.enabled;
}
else if (typeof rawLegacySubagentsSection.enabled === 'boolean') {
subagentsConfig.enabled = rawLegacySubagentsSection.enabled;
}
if (typeof agentsSection.include_in_rules === 'boolean') {
subagentsConfig.include_in_rules =
agentsSection.include_in_rules;
}
else if (typeof rawLegacySubagentsSection.include_in_rules === 'boolean') {
subagentsConfig.include_in_rules =
rawLegacySubagentsSection.include_in_rules;
}
const nestedDefined = typeof raw.nested === 'boolean';
const nested = nestedDefined ? raw.nested : false;
return {
@@ -223,6 +300,7 @@ async function loadConfig(options) {
mcp: globalMcpConfig,
gitignore: gitignoreConfig,
skills: skillsConfig,
subagents: subagentsConfig,
nested,
nestedDefined,
};
+26 -4
View File
@@ -44,6 +44,7 @@ const fs_1 = require("fs");
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const constants_1 = require("../constants");
const SUBAGENTS_DIR_NAME = path.basename(constants_1.RULER_SUBAGENTS_PATH);
/**
* Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
*/
@@ -93,9 +94,18 @@ async function findRulerDir(startPath, checkGlobal = true) {
/**
* Recursively reads all Markdown (.md) files in rulerDir, returning their paths and contents.
* Files are sorted alphabetically by path.
*
* `.ruler/skills/` is always skipped (skills are propagated separately).
* `.ruler/agents/` is skipped unless `options.includeAgents` is `true`.
*/
async function readMarkdownFiles(rulerDir) {
async function readMarkdownFiles(rulerDir, options = {}) {
const mdFiles = [];
const includeAgents = options.includeAgents === true;
// Tracks whether we skipped a `.ruler/agents` subtree so the root-AGENTS.md
// fallback below still recognises ruler content as present and does not
// resurrect a previously generated root AGENTS.md (which may itself contain
// the very agent docs we're now excluding).
let sawExcludedAgents = false;
// Gather all markdown files (recursive) first
async function walk(dir) {
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
@@ -115,13 +125,22 @@ async function readMarkdownFiles(rulerDir) {
}
}
if (isDir) {
// Skip .ruler/skills; skills are propagated separately and should not be concatenated
const relativeFromRoot = path.relative(rulerDir, fullPath);
// Skip .ruler/skills; skills are propagated separately and should not be concatenated
const isSkillsDir = relativeFromRoot === constants_1.SKILLS_DIR ||
relativeFromRoot.startsWith(`${constants_1.SKILLS_DIR}${path.sep}`);
if (isSkillsDir) {
continue;
}
// Skip .ruler/agents unless explicitly opted in via subagents.include_in_rules.
// Subagents are propagated separately to native locations and should not pollute
// the top-level rule concatenation by default.
const isAgentsDir = relativeFromRoot === SUBAGENTS_DIR_NAME ||
relativeFromRoot.startsWith(`${SUBAGENTS_DIR_NAME}${path.sep}`);
if (isAgentsDir && !includeAgents) {
sawExcludedAgents = true;
continue;
}
await walk(fullPath);
}
else if (isFile && entry.name.endsWith('.md')) {
@@ -170,9 +189,12 @@ async function readMarkdownFiles(rulerDir) {
const stat = await fs_1.promises.stat(rootAgentsPath);
if (stat.isFile()) {
const content = await fs_1.promises.readFile(rootAgentsPath, 'utf8');
// Check if this is a generated file and we have other .ruler files
// Check if this is a generated file and we have other .ruler files.
// `sawExcludedAgents` counts as "ruler content present" so a stale
// generated root AGENTS.md isn't resurrected when `.ruler/agents` was
// the only source under `.ruler` and is now being skipped.
const isGenerated = content.startsWith('<!-- Generated by Ruler -->');
const hasRulerFiles = others.length > 0 || primaryFile !== null;
const hasRulerFiles = others.length > 0 || primaryFile !== null || sawExcludedAgents;
// Additional check: if AGENTS.md contains ruler source comments and we have ruler files,
// it's likely a corrupted generated file that should be skipped
const containsRulerSources = content.includes('<!-- Source: .ruler/') ||
+17 -14
View File
@@ -56,25 +56,23 @@ const constants_1 = require("../constants");
async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
const results = [];
const rulerDirConfigs = await processIndependentRulerDirs(rulerDirs);
for (const { rulerDir, files } of rulerDirConfigs) {
// Load config first so we know whether `.ruler/agents/` should be included
// in the rule concatenation for each directory.
for (const rulerDir of rulerDirs) {
const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
const files = await FileSystemUtils.readMarkdownFiles(rulerDir, {
includeAgents: shouldIncludeAgentsInRules(config),
});
results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
}
return results;
}
/**
* Processes each .ruler directory independently, returning configuration for each.
* Each .ruler directory gets its own rules (not merged with others).
* Returns true when `.ruler/agents/*.md` should be concatenated into the
* generated top-level rule files. Defaults to false.
*/
async function processIndependentRulerDirs(rulerDirs) {
const results = [];
// Process each .ruler directory independently
for (const rulerDir of rulerDirs) {
const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
results.push({ rulerDir, files });
}
return results;
function shouldIncludeAgentsInRules(config) {
return config.subagents?.include_in_rules === true;
}
async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
await warnAboutLegacyMcpJson(rulerDir);
@@ -146,6 +144,8 @@ function cloneLoadedConfig(config) {
cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
mcp: config.mcp ? { ...config.mcp } : undefined,
gitignore: config.gitignore ? { ...config.gitignore } : undefined,
skills: config.skills ? { ...config.skills } : undefined,
subagents: config.subagents ? { ...config.subagents } : undefined,
nested: config.nested,
nestedDefined: config.nestedDefined,
};
@@ -203,8 +203,11 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
projectRoot,
configPath,
});
// Read rule files
const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0]);
// Read rule files. `.ruler/agents/` is only included when
// `[agents] include_in_rules = true`.
const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0], {
includeAgents: shouldIncludeAgentsInRules(config),
});
// Concatenate rules
const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(primaryDir));
// Load unified config to get merged MCP configuration
+42 -2
View File
@@ -53,6 +53,23 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
? configSetting
: true; // default to enabled
}
/**
* Resolves subagents enabled state based on precedence:
* CLI flag > ruler.toml > default (disabled).
*
* When neither `[agents] enabled` (nor the legacy `[subagents] enabled`)
* nor a CLI flag is provided, propagation is disabled by default per spec.
* Subagent definitions are an opt-in feature — propagating them silently
* could leak runtime prompts into native subagent locations on projects
* that never intended to use the feature.
*/
function resolveSubagentsEnabled(cliFlag, configSetting) {
return cliFlag !== undefined
? cliFlag
: configSetting !== undefined
? configSetting
: false; // default to disabled — see spec: subagents must opt in
}
/**
* Applies ruler configurations for all supported AI agents.
* @param projectRoot Root directory of the project
@@ -62,7 +79,7 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
* @param projectRoot Root directory of the project
* @param includedAgents Optional list of agent name filters (case-insensitive substrings)
*/
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal) {
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
// Load configuration and rules
(0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
if (configPath) {
@@ -100,6 +117,16 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
await propagateSkills(nestedRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
}
}
// Propagate subagents (mirrors skills handling for nested mode).
const subagentsEnabledResolved = resolveSubagentsEnabled(subagentsEnabled, rootConfig.subagents?.enabled);
{
const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
for (const configEntry of hierarchicalConfigs) {
const nestedRoot = path.dirname(configEntry.rulerDir);
(0, constants_1.logVerbose)(`Propagating subagents for nested directory: ${nestedRoot}`, verbose);
await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, verbose, dryRun);
}
}
generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
}
else {
@@ -117,6 +144,12 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
await propagateSkills(projectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
}
// Propagate subagents (mirrors skills handling).
const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
{
const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, verbose, dryRun);
}
generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
}
// Add skills-generated paths to gitignore if skills are enabled
@@ -126,7 +159,14 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
// Skills enabled by default or explicitly
const { getSkillsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
const skillsPaths = await getSkillsGitignorePaths(projectRoot, selectedAgents);
allGeneratedPaths = [...generatedPaths, ...skillsPaths];
allGeneratedPaths = [...allGeneratedPaths, ...skillsPaths];
}
// Add subagents-generated paths to gitignore if subagents are enabled.
const subagentsEnabledForGitignore = resolveSubagentsEnabled(subagentsEnabled, loadedConfig.subagents?.enabled);
if (subagentsEnabledForGitignore) {
const { getSubagentsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
const subagentPaths = await getSubagentsGitignorePaths(projectRoot, selectedAgents);
allGeneratedPaths = [...allGeneratedPaths, ...subagentPaths];
}
await (0, apply_engine_1.updateGitignore)(projectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun, cliGitignoreLocal);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@intellectronica/ruler",
"version": "0.3.38",
"version": "0.3.40",
"description": "Ruler — apply the same rules to all coding agents",
"main": "dist/lib.js",
"scripts": {