gitea push
This commit is contained in:
+185
-36
@@ -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.
|
||||
|
||||
+3
@@ -58,5 +58,8 @@ class ClaudeAgent extends AbstractAgent_1.AbstractAgent {
|
||||
supportsNativeSkills() {
|
||||
return true;
|
||||
}
|
||||
supportsNativeSubagents() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exports.ClaudeAgent = ClaudeAgent;
|
||||
|
||||
+3
@@ -149,5 +149,8 @@ class CodexCliAgent {
|
||||
supportsNativeSkills() {
|
||||
return true;
|
||||
}
|
||||
supportsNativeSubagents() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exports.CodexCliAgent = CodexCliAgent;
|
||||
|
||||
+3
@@ -42,5 +42,8 @@ class CopilotAgent {
|
||||
supportsNativeSkills() {
|
||||
return true;
|
||||
}
|
||||
supportsNativeSubagents() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exports.CopilotAgent = CopilotAgent;
|
||||
|
||||
+3
@@ -33,5 +33,8 @@ class CursorAgent extends AgentsMdAgent_1.AgentsMdAgent {
|
||||
supportsNativeSkills() {
|
||||
return true;
|
||||
}
|
||||
supportsNativeSubagents() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exports.CursorAgent = CursorAgent;
|
||||
|
||||
+4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user