feat: project rules with .claw/rules/ and multi-framework auto-import
Adds a project rules system for loading instruction files into the
system prompt, plus auto-import from other AI coding frameworks.
- `.claw/rules/*.md` / `.claw/rules/*.txt` / `.claw/rules/*.mdc`
- `.claw/rules.local/*.md` — personal/local rules (gitignored)
- Existing: `CLAUDE.md`, `CLAUDE.local.md`, `.claw/CLAUDE.md`,
`.claw/instructions.md`
When users switch to claw-code from another tool, their existing
rules are automatically detected and loaded:
- Cursor: `.cursorrules`, `.cursor/rules/`
- GitHub Copilot: `.github/copilot-instructions.md`
- Windsurf: `.windsurfrules`, `.windsurfrules/`
- Aider: `.aider.conf.yml` instructions block
- Pi (Plandex): `.plandex/instructions.md`, `.plandex/plan.md`
- OpenCode: `opencode.json` instructions field
- Crush: `.crush/CLAUDE.md`, `.crush/rules/`
`rulesImport` in settings.json controls framework auto-import:
- `"auto"` (default) — import from all detected frameworks
- `"none"` — only load .claw/rules/ and CLAUDE.md files
- `["cursor", "copilot"]` — import only from listed frameworks
```json
{
"rulesImport": "auto"
}
```
💘 Generated with Crush
Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
This commit is contained in:
parent
72f50b840e
commit
5bb811aa5e
|
|
@ -209,6 +209,7 @@ pub struct RuntimeFeatureConfig {
|
||||||
sandbox: SandboxConfig,
|
sandbox: SandboxConfig,
|
||||||
provider_fallbacks: ProviderFallbackConfig,
|
provider_fallbacks: ProviderFallbackConfig,
|
||||||
trusted_roots: Vec<String>,
|
trusted_roots: Vec<String>,
|
||||||
|
<<<<<<< HEAD
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
api_timeout: ApiTimeoutConfig,
|
api_timeout: ApiTimeoutConfig,
|
||||||
rules_import: RulesImportConfig,
|
rules_import: RulesImportConfig,
|
||||||
|
|
@ -324,6 +325,34 @@ impl RuntimeProviderConfig {
|
||||||
pub fn model(&self) -> Option<&str> {
|
pub fn model(&self) -> Option<&str> {
|
||||||
self.model.as_deref()
|
self.model.as_deref()
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
rules_import: RulesImportConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls which external AI coding framework rules are auto-imported
|
||||||
|
/// into the system prompt.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub enum RulesImportConfig {
|
||||||
|
/// Auto-import from all supported frameworks (Cursor, Copilot, Windsurf, Aider)
|
||||||
|
Auto,
|
||||||
|
/// No auto-import — only .claw/rules/ and CLAUDE.md files are loaded
|
||||||
|
None,
|
||||||
|
/// Import only from the listed frameworks
|
||||||
|
List(Vec<String>),
|
||||||
|
#[default]
|
||||||
|
/// Default: auto-import all detected frameworks
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RulesImportConfig {
|
||||||
|
pub fn should_import(&self, framework: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Auto | Self::Default => true,
|
||||||
|
Self::None => false,
|
||||||
|
Self::List(frameworks) => frameworks.iter().any(|f| f.eq_ignore_ascii_case(framework)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ordered chain of fallback model identifiers used when the primary
|
/// Ordered chain of fallback model identifiers used when the primary
|
||||||
|
|
@ -769,6 +798,7 @@ impl ConfigLoader {
|
||||||
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
||||||
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
|
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
|
||||||
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
||||||
|
<<<<<<< HEAD
|
||||||
provider: parse_optional_provider_config(&merged_value)?,
|
provider: parse_optional_provider_config(&merged_value)?,
|
||||||
lsp: parse_optional_lsp_config(&merged_value)?,
|
lsp: parse_optional_lsp_config(&merged_value)?,
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
|
@ -790,6 +820,9 @@ impl ConfigLoader {
|
||||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||||
=======
|
=======
|
||||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||||
|
=======
|
||||||
|
rules_import: parse_optional_rules_import(&merged_value)?,
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
};
|
};
|
||||||
|
|
||||||
ConfigInspection {
|
ConfigInspection {
|
||||||
|
|
@ -1048,6 +1081,7 @@ impl RuntimeConfig {
|
||||||
&self.feature_config.trusted_roots
|
&self.feature_config.trusted_roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
#[must_use]
|
#[must_use]
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
pub fn rules_import(&self) -> &RulesImportConfig {
|
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||||
|
|
@ -1088,6 +1122,11 @@ impl RuntimeConfig {
|
||||||
pub fn lsp_auto_start(&self) -> bool {
|
pub fn lsp_auto_start(&self) -> bool {
|
||||||
self.feature_config.lsp_auto_start
|
self.feature_config.lsp_auto_start
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||||
|
&self.feature_config.rules_import
|
||||||
|
}
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeFeatureConfig {
|
impl RuntimeFeatureConfig {
|
||||||
|
|
@ -2312,6 +2351,7 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
|
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
|
||||||
let Some(object) = root.as_object() else {
|
let Some(object) = root.as_object() else {
|
||||||
return Ok(RulesImportConfig::default());
|
return Ok(RulesImportConfig::default());
|
||||||
|
|
@ -2362,6 +2402,36 @@ fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderCon
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
=======
|
||||||
|
|
||||||
|
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
|
||||||
|
let Some(object) = root.as_object() else {
|
||||||
|
return Ok(RulesImportConfig::Default);
|
||||||
|
};
|
||||||
|
let Some(value) = object.get("rulesImport") else {
|
||||||
|
return Ok(RulesImportConfig::Default);
|
||||||
|
};
|
||||||
|
match value {
|
||||||
|
JsonValue::String(s) => match s.as_str() {
|
||||||
|
"auto" => Ok(RulesImportConfig::Auto),
|
||||||
|
"none" => Ok(RulesImportConfig::None),
|
||||||
|
other => Err(ConfigError::Parse(format!(
|
||||||
|
r#"merged settings.rulesImport: expected "auto", "none", or an array, got "{other}""#
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
JsonValue::Array(arr) => {
|
||||||
|
let frameworks: Vec<String> = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(str::to_owned))
|
||||||
|
.collect();
|
||||||
|
Ok(RulesImportConfig::List(frameworks))
|
||||||
|
}
|
||||||
|
_ => Err(ConfigError::Parse(format!(
|
||||||
|
r#"merged settings.rulesImport: expected "auto", "none", or an array"#
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
||||||
match value {
|
match value {
|
||||||
"off" => Ok(FilesystemIsolationMode::Off),
|
"off" => Ok(FilesystemIsolationMode::Off),
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
||||||
expected: FieldType::StringArray,
|
expected: FieldType::StringArray,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
|
<<<<<<< HEAD
|
||||||
name: "provider",
|
name: "provider",
|
||||||
expected: FieldType::Object,
|
expected: FieldType::Object,
|
||||||
},
|
},
|
||||||
|
|
@ -237,6 +238,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "lspAutoStart",
|
name: "lspAutoStart",
|
||||||
expected: FieldType::Bool,
|
expected: FieldType::Bool,
|
||||||
|
=======
|
||||||
|
name: "rulesImport",
|
||||||
|
expected: FieldType::String,
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,12 +100,18 @@ pub use config::{
|
||||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||||
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||||
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||||
|
<<<<<<< HEAD
|
||||||
=======
|
=======
|
||||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||||
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
|
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
|
||||||
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig,
|
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig,
|
||||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||||
|
=======
|
||||||
|
ProviderFallbackConfig, ResolvedPermissionMode, RulesImportConfig, RuntimeConfig,
|
||||||
|
RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig,
|
||||||
|
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
};
|
};
|
||||||
pub use config_validate::{
|
pub use config_validate::{
|
||||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,7 @@ fn discover_instruction_files(
|
||||||
|
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
for dir in directories {
|
for dir in directories {
|
||||||
|
// Single-file instruction files (existing)
|
||||||
for candidate in [
|
for candidate in [
|
||||||
dir.join("CLAUDE.md"),
|
dir.join("CLAUDE.md"),
|
||||||
dir.join("CLAW.md"),
|
dir.join("CLAW.md"),
|
||||||
|
|
@ -306,13 +307,23 @@ fn discover_instruction_files(
|
||||||
] {
|
] {
|
||||||
push_context_file(&mut files, candidate)?;
|
push_context_file(&mut files, candidate)?;
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
|
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
|
||||||
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
|
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
|
||||||
push_framework_imports(&mut files, &dir, rules_import)?
|
push_framework_imports(&mut files, &dir, rules_import)?
|
||||||
|
=======
|
||||||
|
// .claw/rules/ directory: all .md files loaded in sorted order
|
||||||
|
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
|
||||||
|
// .claw/rules.local/ directory: personal/local rules (gitignored)
|
||||||
|
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
|
||||||
|
// Auto-import from other frameworks (Cursor, Copilot, Windsurf, Aider)
|
||||||
|
push_framework_imports(&mut files, &dir)?;
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
}
|
}
|
||||||
Ok(dedupe_instruction_files(files))
|
Ok(dedupe_instruction_files(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
fn instruction_discovery_dirs(cwd: &Path) -> Vec<PathBuf> {
|
fn instruction_discovery_dirs(cwd: &Path) -> Vec<PathBuf> {
|
||||||
let boundary = nearest_git_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
|
let boundary = nearest_git_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
|
||||||
let mut directories = Vec::new();
|
let mut directories = Vec::new();
|
||||||
|
|
@ -335,6 +346,94 @@ fn nearest_git_root(cwd: &Path) -> Option<PathBuf> {
|
||||||
return Some(dir.to_path_buf());
|
return Some(dir.to_path_buf());
|
||||||
}
|
}
|
||||||
cursor = dir.parent();
|
cursor = dir.parent();
|
||||||
|
=======
|
||||||
|
/// Load all .md files from a rules directory, sorted alphabetically.
|
||||||
|
fn push_rules_dir(files: &mut Vec<ContextFile>, dir: PathBuf) -> std::io::Result<()> {
|
||||||
|
let entries = match fs::read_dir(&dir) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let mut paths: Vec<PathBuf> = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| {
|
||||||
|
p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
|
||||||
|
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
|
||||||
|
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("mdc"))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
for path in paths {
|
||||||
|
push_context_file(files, path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect and import rules from other AI coding frameworks so that
|
||||||
|
/// users switching to claw-code don't have to duplicate their rules.
|
||||||
|
///
|
||||||
|
/// Supported frameworks:
|
||||||
|
/// - Cursor: .cursorrules, .cursor/rules/
|
||||||
|
/// - GitHub Copilot: .github/copilot-instructions.md
|
||||||
|
/// - Windsurf: .windsurfrules, .windsurfrules/
|
||||||
|
/// - Aider: .aider.conf.yml instructions block
|
||||||
|
/// - Pi (Plandex): .plandex/plan.md, .plandex/instructions.md
|
||||||
|
/// - OpenCode: opencode.json instructions field
|
||||||
|
/// - CrushCode / Crush: .crush/rules/, .crush/CLAUDE.md
|
||||||
|
fn push_framework_imports(files: &mut Vec<ContextFile>, dir: &Path) -> std::io::Result<()> {
|
||||||
|
// Cursor
|
||||||
|
push_context_file(files, dir.join(".cursorrules"))?;
|
||||||
|
push_rules_dir(files, dir.join(".cursor").join("rules"))?;
|
||||||
|
// GitHub Copilot
|
||||||
|
push_context_file(files, dir.join(".github").join("copilot-instructions.md"))?;
|
||||||
|
// Windsurf
|
||||||
|
push_context_file(files, dir.join(".windsurfrules"))?;
|
||||||
|
push_rules_dir(files, dir.join(".windsurfrules"))?;
|
||||||
|
// Aider — reads the instruction lines from .aider.conf.yml
|
||||||
|
if let Some(aider_instructions) = read_aider_instructions(dir) {
|
||||||
|
files.push(ContextFile {
|
||||||
|
path: dir.join(".aider.conf.yml").join("instructions"),
|
||||||
|
content: aider_instructions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Pi (Plandex)
|
||||||
|
push_context_file(files, dir.join(".plandex").join("instructions.md"))?;
|
||||||
|
push_context_file(files, dir.join(".plandex").join("plan.md"))?;
|
||||||
|
// OpenCode — reads instructions from opencode.json config
|
||||||
|
if let Some(opencode_instructions) = read_opencode_instructions(dir) {
|
||||||
|
files.push(ContextFile {
|
||||||
|
path: dir.join("opencode.json").join("instructions"),
|
||||||
|
content: opencode_instructions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// CrushCode / Crush
|
||||||
|
push_context_file(files, dir.join(".crush").join("CLAUDE.md"))?;
|
||||||
|
push_rules_dir(files, dir.join(".crush").join("rules"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract instructions from an opencode.json config file.
|
||||||
|
/// OpenCode stores rules in a top-level "instructions" field.
|
||||||
|
fn read_opencode_instructions(dir: &Path) -> Option<String> {
|
||||||
|
let content = fs::read_to_string(dir.join("opencode.json")).ok()?;
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||||
|
parsed.get("instructions")?.as_str().map(str::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract instruction lines from an .aider.conf.yml file.
|
||||||
|
/// Aider stores instructions like: `instructions: ...` or multiline block.
|
||||||
|
fn read_aider_instructions(dir: &Path) -> Option<String> {
|
||||||
|
let content = fs::read_to_string(dir.join(".aider.conf.yml")).ok()?;
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(val) = trimmed.strip_prefix("instructions:") {
|
||||||
|
let instruction = val.trim();
|
||||||
|
if !instruction.is_empty() {
|
||||||
|
return Some(instruction.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue