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,
|
||||
provider_fallbacks: ProviderFallbackConfig,
|
||||
trusted_roots: Vec<String>,
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
api_timeout: ApiTimeoutConfig,
|
||||
rules_import: RulesImportConfig,
|
||||
|
|
@ -324,6 +325,34 @@ impl RuntimeProviderConfig {
|
|||
pub fn model(&self) -> Option<&str> {
|
||||
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
|
||||
|
|
@ -769,6 +798,7 @@ impl ConfigLoader {
|
|||
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
||||
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
|
||||
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
||||
<<<<<<< HEAD
|
||||
provider: parse_optional_provider_config(&merged_value)?,
|
||||
lsp: parse_optional_lsp_config(&merged_value)?,
|
||||
<<<<<<< HEAD
|
||||
|
|
@ -790,6 +820,9 @@ impl ConfigLoader {
|
|||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> 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 {
|
||||
|
|
@ -1048,6 +1081,7 @@ impl RuntimeConfig {
|
|||
&self.feature_config.trusted_roots
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
#[must_use]
|
||||
<<<<<<< HEAD
|
||||
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||
|
|
@ -1088,6 +1122,11 @@ impl RuntimeConfig {
|
|||
pub fn lsp_auto_start(&self) -> bool {
|
||||
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 {
|
||||
|
|
@ -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> {
|
||||
let Some(object) = root.as_object() else {
|
||||
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> {
|
||||
match value {
|
||||
"off" => Ok(FilesystemIsolationMode::Off),
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||
expected: FieldType::StringArray,
|
||||
},
|
||||
FieldSpec {
|
||||
<<<<<<< HEAD
|
||||
name: "provider",
|
||||
expected: FieldType::Object,
|
||||
},
|
||||
|
|
@ -237,6 +238,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||
FieldSpec {
|
||||
name: "lspAutoStart",
|
||||
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)
|
||||
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
|
||||
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig,
|
||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
>>>>>>> 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::{
|
||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ fn discover_instruction_files(
|
|||
|
||||
let mut files = Vec::new();
|
||||
for dir in directories {
|
||||
// Single-file instruction files (existing)
|
||||
for candidate in [
|
||||
dir.join("CLAUDE.md"),
|
||||
dir.join("CLAW.md"),
|
||||
|
|
@ -306,13 +307,23 @@ fn discover_instruction_files(
|
|||
] {
|
||||
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.local"))?;
|
||||
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))
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn instruction_discovery_dirs(cwd: &Path) -> Vec<PathBuf> {
|
||||
let boundary = nearest_git_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
|
||||
let mut directories = Vec::new();
|
||||
|
|
@ -335,6 +346,94 @@ fn nearest_git_root(cwd: &Path) -> Option<PathBuf> {
|
|||
return Some(dir.to_path_buf());
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue