From 27cb1705687ad0d86132061018d988414c6aa194 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 16:49:31 +0200 Subject: [PATCH 1/8] feat(cline): detect .clinerules/*.txt files as ClineRulesFolder Extend the ClineRulesFolder detection to accept .txt files alongside .md files inside the .clinerules directory. This mirrors the CursorRule pattern which accepts both .md and .mdc extensions. Update doc comments in types.rs, cline.rs schema, and cline.rs rules to document the new .txt support. --- crates/agnix-core/src/file_types/detection.rs | 6 ++++-- crates/agnix-core/src/file_types/types.rs | 2 +- crates/agnix-core/src/rules/cline.rs | 7 ++++--- crates/agnix-core/src/schemas/cline.rs | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/agnix-core/src/file_types/detection.rs b/crates/agnix-core/src/file_types/detection.rs index 1d1d8ba35..13b2150db 100644 --- a/crates/agnix-core/src/file_types/detection.rs +++ b/crates/agnix-core/src/file_types/detection.rs @@ -344,8 +344,10 @@ pub fn detect_file_type(path: &Path) -> FileType { } // Roo Code rules (.roo/rules/*.md) name if name.ends_with(".md") && is_under_roo_rules(path) => FileType::RooRules, - // Cline rules folder (.clinerules/*.md) - name if name.ends_with(".md") && parent == Some(".clinerules") => { + // Cline rules folder (.clinerules/*.md, .clinerules/*.txt) + name if (name.ends_with(".md") || name.ends_with(".txt")) + && parent == Some(".clinerules") => + { FileType::ClineRulesFolder } // Windsurf rules (.windsurf/rules/**/*.md) diff --git a/crates/agnix-core/src/file_types/types.rs b/crates/agnix-core/src/file_types/types.rs index ca1f0f381..73cc83410 100644 --- a/crates/agnix-core/src/file_types/types.rs +++ b/crates/agnix-core/src/file_types/types.rs @@ -52,7 +52,7 @@ pub enum FileType { CursorRulesLegacy, /// Cline rules single file (.clinerules) ClineRules, - /// Cline rules folder files (.clinerules/*.md) + /// Cline rules folder files (.clinerules/*.md, .clinerules/*.txt) ClineRulesFolder, /// OpenCode configuration (opencode.json) OpenCodeConfig, diff --git a/crates/agnix-core/src/rules/cline.rs b/crates/agnix-core/src/rules/cline.rs index 7c2e754b8..e8d533e4e 100644 --- a/crates/agnix-core/src/rules/cline.rs +++ b/crates/agnix-core/src/rules/cline.rs @@ -1,9 +1,10 @@ -//! Cline rules validation rules (CLN-001 to CLN-003) +//! Cline rules validation rules (CLN-001 to CLN-004) //! //! Validates: //! - CLN-001: Empty clinerules file (HIGH) - files must have content //! - CLN-002: Invalid paths glob in clinerules (HIGH) - glob patterns must be valid //! - CLN-003: Unknown frontmatter key in clinerules (MEDIUM) - only `paths` is recognized +//! - CLN-004: Scalar paths in clinerules (HIGH) - must be array, not scalar use crate::{ FileType, @@ -61,7 +62,7 @@ impl Validator for ClineValidator { // CLN-001: Empty clinerules file (ERROR) if config.is_rule_enabled("CLN-001") { if is_folder { - // For folder .md files, check body after frontmatter if present + // For folder files (.md/.txt), check body after frontmatter if present if let Some(parsed) = parse_frontmatter(content) { // Only check body emptiness when frontmatter parsed successfully; // parse errors (e.g. missing closing ---) produce empty body by default @@ -108,7 +109,7 @@ impl Validator for ClineValidator { } } - // CLN-002 and CLN-003 only apply to folder .md files (they have frontmatter) + // CLN-002 and CLN-003 only apply to folder files (.md/.txt) (they have frontmatter) if !is_folder { return diagnostics; } diff --git a/crates/agnix-core/src/schemas/cline.rs b/crates/agnix-core/src/schemas/cline.rs index 9835f0e9c..fe0dd5ae4 100644 --- a/crates/agnix-core/src/schemas/cline.rs +++ b/crates/agnix-core/src/schemas/cline.rs @@ -2,7 +2,7 @@ //! //! Provides parsing and validation for: //! - `.clinerules` single file (plain text, no frontmatter) -//! - `.clinerules/*.md` folder files (optional `paths` frontmatter) +//! - `.clinerules/*.md` and `.clinerules/*.txt` folder files (optional `paths` frontmatter) //! //! Folder files support YAML frontmatter with a `paths` field //! containing glob patterns for scoped rule application. From e1e4a60e4aa389e09e3d58cfb4445540f4686891 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 16:50:31 +0200 Subject: [PATCH 2/8] docs(cline): update WASM bindings and docs for .txt file support Add .clinerules/example.md and .clinerules/example.txt entries to the WASM get_supported_file_types list. Update SPEC.md file support table and VALIDATION-RULES.md rule descriptions for CLN-002, CLN-003, and CLN-004 to mention .txt files alongside .md. --- SPEC.md | 2 +- crates/agnix-wasm/src/lib.rs | 2 ++ knowledge-base/VALIDATION-RULES.md | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index d189fbc84..fcd0bc623 100644 --- a/SPEC.md +++ b/SPEC.md @@ -20,7 +20,7 @@ | References | @imports | 4 | | GitHub Copilot | .github/copilot-instructions.md, .github/instructions/*.instructions.md, .github/agents/*.agent.md, .github/prompts/*.prompt.md, .github/hooks/hooks.json, .github/workflows/copilot-setup-steps.yml | 17 | | Cursor Project Rules | .cursor/rules/*.mdc, .cursorrules, .cursor/hooks.json, .cursor/agents/**/*.md, .cursor/environment.json | 16 | -| Cline | .clinerules, .clinerules/*.md | 4 | +| Cline | .clinerules, .clinerules/*.md, .clinerules/*.txt | 4 | | OpenCode | opencode.json | 8 | | Gemini CLI | GEMINI.md, GEMINI.local.md, .gemini/settings.json (hooks), gemini-extension.json (extensions), .geminiignore | 9 | | Codex CLI | .codex/config.toml | 6 | diff --git a/crates/agnix-wasm/src/lib.rs b/crates/agnix-wasm/src/lib.rs index 69bbbb602..484ac5e14 100644 --- a/crates/agnix-wasm/src/lib.rs +++ b/crates/agnix-wasm/src/lib.rs @@ -149,6 +149,8 @@ pub fn get_supported_file_types() -> JsValue { (".github/copilot-instructions.md", "Copilot"), ("GEMINI.md", "GeminiMd"), (".clinerules", "ClineRules"), + (".clinerules/example.md", "ClineRulesFolder"), + (".clinerules/example.txt", "ClineRulesFolder"), ("CODEX.md", "Codex"), (".opencode/instructions.md", "OpenCode"), ("mcp.json", "Mcp"), diff --git a/knowledge-base/VALIDATION-RULES.md b/knowledge-base/VALIDATION-RULES.md index 68ce53661..44531dee9 100644 --- a/knowledge-base/VALIDATION-RULES.md +++ b/knowledge-base/VALIDATION-RULES.md @@ -1322,21 +1322,21 @@ Rules with an empty `applies_to` object (`{}`) apply universally. ### CLN-002 [HIGH] Invalid Paths Glob in Cline Rules -**Requirement**: `paths` field in `.clinerules/*.md` frontmatter MUST contain valid glob patterns +**Requirement**: `paths` field in `.clinerules/*.md` and `.clinerules/*.txt` frontmatter MUST contain valid glob patterns **Detection**: Parse YAML frontmatter, extract `paths` field, validate each glob pattern **Fix**: No auto-fix (glob patterns must be manually corrected) **Source**: docs.cline.bot/improving-your-workflow/cline-rules ### CLN-003 [MEDIUM] Unknown Frontmatter Key in Cline Rules -**Requirement**: Frontmatter in `.clinerules/*.md` files SHOULD only use documented keys (`paths`) +**Requirement**: Frontmatter in `.clinerules/*.md` and `.clinerules/*.txt` files SHOULD only use documented keys (`paths`) **Detection**: Parse YAML frontmatter, check all keys against allowlist **Fix**: [AUTO-FIX unsafe] Remove unknown frontmatter keys **Source**: docs.cline.bot/improving-your-workflow/cline-rules ### CLN-004 [HIGH] Scalar Paths in Cline Rules -**Requirement**: `paths` field in `.clinerules/*.md` frontmatter MUST be a YAML array, not a scalar string +**Requirement**: `paths` field in `.clinerules/*.md` and `.clinerules/*.txt` frontmatter MUST be a YAML array, not a scalar string **Detection**: Parse YAML frontmatter, check if `paths` is a scalar string (Cline silently ignores scalar values) **Fix**: [AUTO-FIX safe] Convert scalar paths to array format **Source**: docs.cline.bot/features/cline-rules From 637a40216291ad08c039b751122e1d33e0823dfa Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 16:53:46 +0200 Subject: [PATCH 3/8] test(cline): add .txt file detection tests, validation tests, and fixtures Add test fixtures for .clinerules/*.txt files (valid and invalid cases mirroring the existing .md fixtures). Add detection tests verifying .txt files are classified as ClineRulesFolder and .json files are rejected. Add validation tests for CLN-001 through CLN-004 using .txt paths. Add benchmark entries for .clinerules folder detection. --- crates/agnix-core/benches/validation.rs | 8 ++ crates/agnix-core/src/file_types/detection.rs | 20 +++++ crates/agnix-core/src/rules/cline.rs | 73 +++++++++++++++++++ .../cline-invalid/.clinerules/bad-glob.txt | 7 ++ .../.clinerules/scalar-paths.txt | 6 ++ .../.clinerules/unknown-keys.txt | 9 +++ .../fixtures/cline/.clinerules/03-python.txt | 13 ++++ 7 files changed, 136 insertions(+) create mode 100644 tests/fixtures/cline-invalid/.clinerules/bad-glob.txt create mode 100644 tests/fixtures/cline-invalid/.clinerules/scalar-paths.txt create mode 100644 tests/fixtures/cline-invalid/.clinerules/unknown-keys.txt create mode 100644 tests/fixtures/cline/.clinerules/03-python.txt diff --git a/crates/agnix-core/benches/validation.rs b/crates/agnix-core/benches/validation.rs index d5439b09b..12380c1ad 100644 --- a/crates/agnix-core/benches/validation.rs +++ b/crates/agnix-core/benches/validation.rs @@ -561,6 +561,14 @@ fn bench_is_instruction_file(c: &mut Criterion) { ("agents_md", Path::new("AGENTS.md").to_path_buf()), ("gemini_md", Path::new("gemini.md").to_path_buf()), ("clinerules", Path::new(".clinerules").to_path_buf()), + ( + "clinerules_folder_md", + Path::new(".clinerules/rules.md").to_path_buf(), + ), + ( + "clinerules_folder_txt", + Path::new(".clinerules/rules.txt").to_path_buf(), + ), // Directory-based matches ( "cursor_mdc", diff --git a/crates/agnix-core/src/file_types/detection.rs b/crates/agnix-core/src/file_types/detection.rs index 13b2150db..66e8297d3 100644 --- a/crates/agnix-core/src/file_types/detection.rs +++ b/crates/agnix-core/src/file_types/detection.rs @@ -834,6 +834,26 @@ mod tests { ); } + #[test] + fn detect_cline_rules_folder_txt() { + assert_eq!( + detect_file_type(Path::new(".clinerules/custom.txt")), + FileType::ClineRulesFolder + ); + assert_eq!( + detect_file_type(Path::new(".clinerules/01-coding.txt")), + FileType::ClineRulesFolder + ); + } + + #[test] + fn detect_cline_rules_folder_non_md_txt_rejected() { + assert_ne!( + detect_file_type(Path::new(".clinerules/config.json")), + FileType::ClineRulesFolder + ); + } + #[test] fn detect_opencode_config() { assert_eq!( diff --git a/crates/agnix-core/src/rules/cline.rs b/crates/agnix-core/src/rules/cline.rs index e8d533e4e..da6c2fe4d 100644 --- a/crates/agnix-core/src/rules/cline.rs +++ b/crates/agnix-core/src/rules/cline.rs @@ -245,6 +245,15 @@ mod tests { validator.validate(Path::new(".clinerules/typescript.md"), content, config) } + fn validate_folder_txt(content: &str) -> Vec { + let validator = ClineValidator; + validator.validate( + Path::new(".clinerules/python.txt"), + content, + &LintConfig::default(), + ) + } + // ===== CLN-001: Empty Clinerules File ===== #[test] @@ -593,4 +602,68 @@ unknownKey: value FileType::ClineRulesFolder ); } + + // ===== .txt file validation (mirrors .md tests) ===== + + #[test] + fn test_cln_001_empty_txt_file() { + let diagnostics = validate_folder_txt(""); + let cln_001: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-001").collect(); + assert_eq!(cln_001.len(), 1); + assert_eq!(cln_001[0].level, DiagnosticLevel::Error); + } + + #[test] + fn test_cln_001_valid_txt_file() { + let content = "---\npaths:\n - \"**/*.py\"\n---\n# Python Rules\n\nFollow PEP 8.\n"; + let diagnostics = validate_folder_txt(content); + let cln_001: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-001").collect(); + assert!(cln_001.is_empty()); + } + + #[test] + fn test_cln_002_bad_glob_in_txt() { + let content = "---\npaths:\n - \"[unclosed\"\n---\n# Instructions\n"; + let diagnostics = validate_folder_txt(content); + let cln_002: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-002").collect(); + assert_eq!(cln_002.len(), 1); + assert_eq!(cln_002[0].level, DiagnosticLevel::Error); + } + + #[test] + fn test_cln_003_unknown_keys_in_txt() { + let content = "---\npaths:\n - \"**/*.ts\"\nunknownKey: value\n---\n# Instructions\n"; + let diagnostics = validate_folder_txt(content); + let cln_003: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-003").collect(); + assert_eq!(cln_003.len(), 1); + assert_eq!(cln_003[0].level, DiagnosticLevel::Warning); + } + + #[test] + fn test_cln_004_scalar_paths_in_txt() { + let content = "---\npaths: \"**/*.ts\"\n---\n# Instructions\n"; + let diagnostics = validate_folder_txt(content); + let cln_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-004").collect(); + assert_eq!(cln_004.len(), 1); + assert_eq!(cln_004[0].level, DiagnosticLevel::Error); + } + + #[test] + fn test_valid_txt_no_diagnostics() { + let content = "---\npaths:\n - \"**/*.py\"\n---\n# Python Guidelines\n\nAlways use type hints.\n"; + let diagnostics = validate_folder_txt(content); + assert!( + diagnostics.is_empty(), + "Expected no diagnostics for valid .txt file, got: {:?}", + diagnostics + ); + } + + #[test] + fn test_txt_file_detection() { + assert_eq!( + crate::detect_file_type(Path::new(".clinerules/python.txt")), + FileType::ClineRulesFolder + ); + } } diff --git a/tests/fixtures/cline-invalid/.clinerules/bad-glob.txt b/tests/fixtures/cline-invalid/.clinerules/bad-glob.txt new file mode 100644 index 000000000..748355d0f --- /dev/null +++ b/tests/fixtures/cline-invalid/.clinerules/bad-glob.txt @@ -0,0 +1,7 @@ +--- +paths: + - "[unclosed" +--- +# Bad Glob Test + +This file has an invalid glob pattern. diff --git a/tests/fixtures/cline-invalid/.clinerules/scalar-paths.txt b/tests/fixtures/cline-invalid/.clinerules/scalar-paths.txt new file mode 100644 index 000000000..08677e0f9 --- /dev/null +++ b/tests/fixtures/cline-invalid/.clinerules/scalar-paths.txt @@ -0,0 +1,6 @@ +--- +paths: "**/*.ts" +--- +# Scalar Paths Test + +This file uses scalar paths which Cline ignores. diff --git a/tests/fixtures/cline-invalid/.clinerules/unknown-keys.txt b/tests/fixtures/cline-invalid/.clinerules/unknown-keys.txt new file mode 100644 index 000000000..8d3bf9cc4 --- /dev/null +++ b/tests/fixtures/cline-invalid/.clinerules/unknown-keys.txt @@ -0,0 +1,9 @@ +--- +paths: + - "**/*.ts" +unknownKey: value +anotherBadKey: 123 +--- +# Unknown Keys Test + +This file has unknown frontmatter keys. diff --git a/tests/fixtures/cline/.clinerules/03-python.txt b/tests/fixtures/cline/.clinerules/03-python.txt new file mode 100644 index 000000000..ed6f36ae2 --- /dev/null +++ b/tests/fixtures/cline/.clinerules/03-python.txt @@ -0,0 +1,13 @@ +--- +paths: + - "**/*.py" +--- +# Python Rules + +Follow PEP 8 style guidelines. + +## Guidelines + +- Use type hints for function signatures +- Prefer f-strings over format() +- Use pathlib for file system operations From efce1d2e231186edc5a32c7b982e62c4996da83a Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 16:59:25 +0200 Subject: [PATCH 4/8] fix: update stale doc comments in schemas/cline.rs for .txt support --- crates/agnix-core/src/schemas/cline.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agnix-core/src/schemas/cline.rs b/crates/agnix-core/src/schemas/cline.rs index fe0dd5ae4..eaecb448f 100644 --- a/crates/agnix-core/src/schemas/cline.rs +++ b/crates/agnix-core/src/schemas/cline.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; -/// Known valid keys for .clinerules/*.md frontmatter +/// Known valid keys for .clinerules folder file frontmatter const KNOWN_KEYS: &[&str] = &["paths"]; /// Paths field can be a single string (scalar) or an array of strings. @@ -46,7 +46,7 @@ impl PathsField { } } -/// Frontmatter schema for Cline .clinerules/*.md files +/// Frontmatter schema for Cline .clinerules folder files #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ClineRuleSchema { /// Glob patterns specifying which files this rule applies to @@ -93,7 +93,7 @@ pub struct GlobValidation { pub error: Option, } -/// Parse frontmatter from a Cline .clinerules/*.md file +/// Parse frontmatter from a Cline .clinerules folder file /// /// Returns parsed frontmatter if present, or None if no frontmatter exists. pub fn parse_frontmatter(content: &str) -> Option { From 99297dec43fc98dae240079ab946d147742555f8 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 17:07:25 +0200 Subject: [PATCH 5/8] fix: strengthen .txt test assertions and fix review findings - Add message content assertions to CLN-001/002/003/004 .txt tests - CLN-003 .txt test now uses two unknown keys with fix/safety assertions - CLN-004 .txt test now verifies autofix presence, safety, and format - Remove duplicate test_txt_file_detection (covered in detection.rs) - Add .yaml and .toml to negative detection test - Fix comment on line 112 to mention CLN-004 --- crates/agnix-core/src/file_types/detection.rs | 8 +++++ crates/agnix-core/src/rules/cline.rs | 31 +++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/agnix-core/src/file_types/detection.rs b/crates/agnix-core/src/file_types/detection.rs index 66e8297d3..a369a65a8 100644 --- a/crates/agnix-core/src/file_types/detection.rs +++ b/crates/agnix-core/src/file_types/detection.rs @@ -852,6 +852,14 @@ mod tests { detect_file_type(Path::new(".clinerules/config.json")), FileType::ClineRulesFolder ); + assert_ne!( + detect_file_type(Path::new(".clinerules/config.yaml")), + FileType::ClineRulesFolder + ); + assert_ne!( + detect_file_type(Path::new(".clinerules/config.toml")), + FileType::ClineRulesFolder + ); } #[test] diff --git a/crates/agnix-core/src/rules/cline.rs b/crates/agnix-core/src/rules/cline.rs index da6c2fe4d..0737a4465 100644 --- a/crates/agnix-core/src/rules/cline.rs +++ b/crates/agnix-core/src/rules/cline.rs @@ -109,7 +109,7 @@ impl Validator for ClineValidator { } } - // CLN-002 and CLN-003 only apply to folder files (.md/.txt) (they have frontmatter) + // CLN-002, CLN-003, and CLN-004 only apply to folder files (.md/.txt) (they have frontmatter) if !is_folder { return diagnostics; } @@ -611,6 +611,7 @@ unknownKey: value let cln_001: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-001").collect(); assert_eq!(cln_001.len(), 1); assert_eq!(cln_001[0].level, DiagnosticLevel::Error); + assert!(cln_001[0].message.contains("empty")); } #[test] @@ -628,15 +629,24 @@ unknownKey: value let cln_002: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-002").collect(); assert_eq!(cln_002.len(), 1); assert_eq!(cln_002[0].level, DiagnosticLevel::Error); + assert!(cln_002[0].message.contains("Invalid glob pattern")); } #[test] fn test_cln_003_unknown_keys_in_txt() { - let content = "---\npaths:\n - \"**/*.ts\"\nunknownKey: value\n---\n# Instructions\n"; + let content = + "---\npaths:\n - \"**/*.ts\"\nunknownKey: value\nanotherBadKey: 123\n---\n# Instructions\n"; let diagnostics = validate_folder_txt(content); let cln_003: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-003").collect(); - assert_eq!(cln_003.len(), 1); + assert_eq!(cln_003.len(), 2); assert_eq!(cln_003[0].level, DiagnosticLevel::Warning); + assert!(cln_003.iter().any(|d| d.message.contains("unknownKey"))); + assert!(cln_003.iter().any(|d| d.message.contains("anotherBadKey"))); + assert!( + cln_003.iter().all(|d| d.has_fixes()), + "All unknown key diagnostics should include deletion fixes" + ); + assert!(cln_003.iter().all(|d| !d.fixes[0].safe)); } #[test] @@ -646,6 +656,14 @@ unknownKey: value let cln_004: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-004").collect(); assert_eq!(cln_004.len(), 1); assert_eq!(cln_004[0].level, DiagnosticLevel::Error); + assert!(cln_004[0].message.contains("scalar")); + assert!(cln_004[0].has_fixes(), "CLN-004 should have an auto-fix"); + assert!(cln_004[0].fixes[0].safe, "CLN-004 fix should be safe"); + assert!( + cln_004[0].fixes[0].replacement.contains("- \"**/*.ts\""), + "Fix should convert scalar to array format, got: {}", + cln_004[0].fixes[0].replacement + ); } #[test] @@ -659,11 +677,4 @@ unknownKey: value ); } - #[test] - fn test_txt_file_detection() { - assert_eq!( - crate::detect_file_type(Path::new(".clinerules/python.txt")), - FileType::ClineRulesFolder - ); - } } From 22800e57eb43f019f37fa9504f3995ddc61c1a2d Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 17:15:32 +0200 Subject: [PATCH 6/8] docs: add changelog entry for .clinerules/*.txt support --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da968d584..a4df28634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **`.clinerules/*.txt` file support**: agnix now detects and validates `.clinerules/*.txt` files as `ClineRulesFolder` file type. Rules CLN-002 (invalid paths glob), CLN-003 (unknown frontmatter key), and CLN-004 (scalar paths) now apply to `.txt` files in addition to `.md` files, matching Cline's actual behavior. + ### Changed - **Unified design system**: Import shared design tokens from agent-sh/design-system. Switch from Outfit to Inter font. Add font preconnect hints to Docusaurus config. Keeps teal accent and light/dark mode. From 7fcff39dcd9860aa3e0bcadf9aa3db2582971a21 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 17:23:02 +0200 Subject: [PATCH 7/8] style: fix cargo fmt formatting in cline.rs tests --- crates/agnix-core/src/rules/cline.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/agnix-core/src/rules/cline.rs b/crates/agnix-core/src/rules/cline.rs index 0737a4465..faf81a1c8 100644 --- a/crates/agnix-core/src/rules/cline.rs +++ b/crates/agnix-core/src/rules/cline.rs @@ -634,8 +634,7 @@ unknownKey: value #[test] fn test_cln_003_unknown_keys_in_txt() { - let content = - "---\npaths:\n - \"**/*.ts\"\nunknownKey: value\nanotherBadKey: 123\n---\n# Instructions\n"; + let content = "---\npaths:\n - \"**/*.ts\"\nunknownKey: value\nanotherBadKey: 123\n---\n# Instructions\n"; let diagnostics = validate_folder_txt(content); let cln_003: Vec<_> = diagnostics.iter().filter(|d| d.rule == "CLN-003").collect(); assert_eq!(cln_003.len(), 2); @@ -668,7 +667,8 @@ unknownKey: value #[test] fn test_valid_txt_no_diagnostics() { - let content = "---\npaths:\n - \"**/*.py\"\n---\n# Python Guidelines\n\nAlways use type hints.\n"; + let content = + "---\npaths:\n - \"**/*.py\"\n---\n# Python Guidelines\n\nAlways use type hints.\n"; let diagnostics = validate_folder_txt(content); assert!( diagnostics.is_empty(), @@ -676,5 +676,4 @@ unknownKey: value diagnostics ); } - } From 3e34913ca274f5fcbfe9ec8c229111352630288a Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Thu, 26 Feb 2026 17:27:30 +0200 Subject: [PATCH 8/8] fix: include CLN-001 in changelog entry per review feedback --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1acaa0494..57a2656dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **`.clinerules/*.txt` file support**: agnix now detects and validates `.clinerules/*.txt` files as `ClineRulesFolder` file type. Rules CLN-002 (invalid paths glob), CLN-003 (unknown frontmatter key), and CLN-004 (scalar paths) now apply to `.txt` files in addition to `.md` files, matching Cline's actual behavior. +- **`.clinerules/*.txt` file support**: agnix now detects and validates `.clinerules/*.txt` files as `ClineRulesFolder` file type. Rules CLN-001 through CLN-004 now apply to `.txt` files in addition to `.md` files, matching Cline's actual behavior. ### Fixed - **CUR-016 environment.json validation rewritten**: Schema now matches the current Cursor Cloud Agent spec - `install` is required, `terminals` is optional (was previously required), and `build` (with `dockerfile` and `context`) and `update` fields are now validated. Snapshot-based approach replaced with field-level structural validation. Includes 12 new unit tests.