From 5bb811aa5ed39e924327fa7f25b238b7b7f4726c Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 28 Apr 2026 11:03:45 -0500 Subject: [PATCH] feat: project rules with .claw/rules/ and multi-framework auto-import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rust/crates/runtime/src/config.rs | 70 +++++++++++++++ rust/crates/runtime/src/config_validate.rs | 5 ++ rust/crates/runtime/src/lib.rs | 6 ++ rust/crates/runtime/src/prompt.rs | 99 ++++++++++++++++++++++ 4 files changed, 180 insertions(+) diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index 575f9931..b2b0a36d 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -209,6 +209,7 @@ pub struct RuntimeFeatureConfig { sandbox: SandboxConfig, provider_fallbacks: ProviderFallbackConfig, trusted_roots: Vec, +<<<<<<< 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), + #[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, ConfigE ) } +<<<<<<< HEAD fn parse_optional_rules_import(root: &JsonValue) -> Result { let Some(object) = root.as_object() else { return Ok(RulesImportConfig::default()); @@ -2362,6 +2402,36 @@ fn parse_optional_provider_config(root: &JsonValue) -> Result Result { + 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 = 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 { match value { "off" => Ok(FilesystemIsolationMode::Off), diff --git a/rust/crates/runtime/src/config_validate.rs b/rust/crates/runtime/src/config_validate.rs index 525e8c14..64dd33b8 100644 --- a/rust/crates/runtime/src/config_validate.rs +++ b/rust/crates/runtime/src/config_validate.rs @@ -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) }, ]; diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index db21af34..0287c25e 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -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, diff --git a/rust/crates/runtime/src/prompt.rs b/rust/crates/runtime/src/prompt.rs index e62e32ea..8cef3200 100644 --- a/rust/crates/runtime/src/prompt.rs +++ b/rust/crates/runtime/src/prompt.rs @@ -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 { 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 { 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, 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 = 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, 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 { + 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 { + 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 }