Skip to content

feat: three-tier rules system#1777

Open
Nemo4110 wants to merge 9 commits intoMoonshotAI:mainfrom
Nemo4110:feat/agent-rules
Open

feat: three-tier rules system#1777
Nemo4110 wants to merge 9 commits intoMoonshotAI:mainfrom
Nemo4110:feat/agent-rules

Conversation

@Nemo4110
Copy link
Copy Markdown

@Nemo4110 Nemo4110 commented Apr 7, 2026

Related Issue

Resolve #1747

Description

This PR introduces a three-tier rules system for managing development guidelines and coding standards in Kimi Code CLI.

Features

  • Three-tier hierarchy: builtin → user (~/.config/agents/rules/) → project (.agents/rules/)
  • YAML frontmatter support: name, description, paths, priority, extends
  • Automatic rule injection: Active rules are injected into system prompt with configurable limits (32 KiB size, 10 count)
  • Slash commands: /rules [list|show|on|off|reset] for rule management
  • State persistence: Per-project rule states stored in .agents/rules.state.toml
  • Path-based matching: Rules automatically apply based on file path patterns

Built-in Rules

  • common/coding-style: General coding conventions
  • common/testing: Testing best practices
  • python/coding-style: Python-specific guidelines

Configuration

[rules]
enable = true
auto_enable = true
max_rules = 10
max_size = 32768

Technical Details

New modules in src/kimi_cli/rules/:

  • models.py: Data models (Rule, RuleMetadata, RuleState)
  • parser.py: YAML frontmatter parsing and rule file parsing
  • discovery.py: Rule file discovery across all levels
  • registry.py: Rule management, enable/disable, state handling
  • injector.py: System prompt injection with size limits
  • state.py: State persistence to TOML

Slash commands in src/kimi_cli/soul/slash.py:

  • /rules list [level] [--all]: List rules and their status
  • /rules show : View rule content
  • /rules on|off : Enable/disable rules
  • /rules reset [--hard]: Reset to defaults

Testing

Added comprehensive tests in tests/core/test_rules.py covering:

  • Frontmatter parsing (valid, empty, invalid YAML)
  • Rule file parsing with defaults
  • Path matching logic (glob patterns, Windows paths)
  • State persistence (save/load/clear)
  • Registry operations (enable/disable, filtering, stats)
  • Prompt injection formatting and size limits
  • Rule discovery across directories

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works. (also test with uv run kimi)
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open with Devin

041 added 3 commits April 7, 2026 10:43
Implement a comprehensive rules system to manage development guidelines
and coding standards across builtin, user, and project levels.

Features:
- Three-tier rule hierarchy: builtin (~/.config/agents/rules/) -> user -> project (.agents/rules/)
- YAML frontmatter support for rule metadata (name, description, paths, priority, extends)
- /rules slash commands for listing, viewing, and toggling rules (list, show, on, off, reset)
- Automatic rule detection based on file path patterns
- State persistence in .agents/rules.state.toml
- System prompt injection with size (32 KiB) and count (10) limits

Includes:
- Bug fixes for async/await issues and KaosPath conversion
- UI polish for /rules list display format

New modules: src/kimi_cli/rules/ (models, discovery, parser, registry, injector, state)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2ffc0ccf8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +119 to +123
for rule_id, state in self._states.items():
# Pinned states go to user level unless they came from project
# For now, save all to user level for simplicity
# TODO: Track origin level and save accordingly
user_states[rule_id] = state
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Save project-scoped states to project state file

RulesStateManager.save() currently copies every in-memory state into user_states, so a /rules on|off action in one repository is persisted to ~/.config/agents/rules.state.toml and leaks into unrelated projects. That breaks the documented per-project override behavior and can silently change rule enablement globally; project-origin states need to be tracked and written to .agents/rules.state.toml instead of always going to user scope.

Useful? React with 👍 / 👎.

Comment on lines +126 to +127
if user_states:
await self._save_state_file(self.user_state_path, user_states)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist reset by writing an empty rules state

After /rules reset, the registry clears in-memory states and calls save_states(), but save() skips writing when user_states is empty. If a state file already exists, this leaves stale entries on disk, so the reset is effectively undone on the next session load. save() should overwrite/delete existing state files when no states remain so reset persists across restarts.

Useful? React with 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

041 added 2 commits April 7, 2026 12:00
- Add level field to RuleState for state separation by user/project
- Fix rule loading priority: project rules now correctly override builtin
- Fix injector priority to respect hierarchy (project > user > builtin)
- Implement --hard reset to delete state files instead of saving empty
- Delete empty state files on save to prevent stale data after reset
- Combine nested if statements and use ternary operators for clarity
- Test level separation of user/project states
- Test project state priority over user state
- Test empty states delete files
- Test delete_state_files functionality
@Nemo4110
Copy link
Copy Markdown
Author

Nemo4110 commented Apr 7, 2026

Above mentioned comments were fixed by new 2 commits.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

```

Shell mode also supports some slash commands, including `/help`, `/exit`, `/version`, `/editor`, `/changelog`, `/feedback`, `/export`, `/import`, and `/task`.
Shell mode also supports some slash commands, including `/help`, `/exit`, `/version`, `/editor`, `/changelog`, `/feedback`, `/export`, `/import`, `/rules`, and `/task`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Documentation claims /rules works in shell mode but no shell-mode registration exists

The docs at docs/en/guides/interaction.md:22, docs/zh/guides/interaction.md:22, docs/en/reference/slash-commands.md:6, and docs/zh/reference/slash-commands.md:6 all list /rules as available in shell mode. However, /rules is only registered in the soul-level registry at src/kimi_cli/soul/slash.py:293, not in the shell-mode shell_mode_registry at src/kimi_cli/ui/shell/slash.py:36. Other commands listed as shell-mode-available (e.g., /export, /import, /task) have explicit @shell_mode_registry.command registrations in src/kimi_cli/ui/shell/. When a user types /rules in shell mode, the shell UI will display "not available in shell mode" instead of running the command.

Prompt for agents
The /rules command is documented as available in shell mode (in docs/en/guides/interaction.md, docs/zh/guides/interaction.md, docs/en/reference/slash-commands.md, and docs/zh/reference/slash-commands.md) but it's not registered in the shell_mode_registry. Either add a @shell_mode_registry.command registration for /rules in src/kimi_cli/ui/shell/slash.py (following the pattern used by /export, /import, /task in src/kimi_cli/ui/shell/export_import.py and src/kimi_cli/ui/shell/slash.py), or remove /rules from the shell-mode command lists in all four documentation files.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Signed-off-by: T0M0R1N <70043311+Nemo4110@users.noreply.github.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8396c2382

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +225 to +226
state = self._state.get(rule_id, RuleState())
if not state.enabled:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect auto-enable filter when state is absent

_auto_enable_rules() only creates state entries for matching categories, but get_active_rules() treats missing state as enabled=True, so every rule without an explicit state is still injected. In practice this makes auto_enable_by_path ineffective: language-specific rules that do not match the current project remain active unless users manually disable them, which can bloat prompts and apply wrong guidance.

Useful? React with 👍 / 👎.

metadata = RuleMetadata(
name=metadata_dict.get("name"),
description=metadata_dict.get("description"),
paths=metadata_dict.get("paths", []),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize scalar paths frontmatter into a list

The parser assigns metadata.paths directly from YAML without type normalization. If a rule uses paths: "**/*.py" (a common single-pattern form), should_apply_rule() iterates the string character-by-character; because '*' is evaluated as a glob, the rule can match essentially any file and become globally active. Coercing scalar strings to a one-item list avoids this incorrect broad application.

Useful? React with 👍 / 👎.

Comment on lines +112 to +113
if root_str.startswith(work_dir_str):
return "project"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect project roots by path ancestry, not prefix

Level detection uses root_str.startswith(work_dir_str), which misclassifies user rules as project rules when the working directory is an ancestor of the user config path (for example, running in $HOME makes ~/.config/agents/rules look project-scoped). That causes wrong level reporting and persists toggles to the project state file instead of the user state file in those sessions.

Useful? React with 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

041 added 2 commits April 7, 2026 14:14
- Fix KaosPath.to_path() → unsafe_to_local_path() (was causing all rule loading to fail)
- Fix max_total_size=0 to mean unlimited (was incorrectly falling back to default)
- Add max_active_rules enforcement (was not being applied)
- Remove /rules from shell mode documentation (command only available in agent mode)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 595cdebcf1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

name=metadata_dict.get("name"),
description=metadata_dict.get("description"),
paths=metadata_dict.get("paths", []),
priority=metadata_dict.get("priority", 100),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate frontmatter priority before storing metadata

This assigns priority directly from YAML without type coercion, so a user rule like priority: "10" (or any non-integer) leaves RuleMetadata.priority as a string. Later, both RulesRegistry.get_all_rules() and Runtime.create() sort mixed builtin integer priorities with that string value, which raises TypeError and can break startup or /rules listing whenever such a rule is present.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 11 additional findings in Devin Review.

Open in Devin Review

Comment on lines +183 to +194
async def _any_file_exists(self, pattern: str) -> bool:
"""Check if any file matching pattern exists in work_dir."""
import fnmatch

try:
# KaosPath has glob() but not rglob(), use **/* for recursive
async for item in self.work_dir.glob("**/*"):
if fnmatch.fnmatch(item.name, pattern):
return True
except Exception:
pass
return False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Expensive recursive glob scans in _auto_enable_rules produce no observable effect

_detect_project_types() at src/kimi_cli/rules/registry.py:153 iterates all ~11 language categories, calling _any_file_exists() for each pattern. Each call does self.work_dir.glob("**/*") — a full recursive directory traversal — then matches filenames via fnmatch. For non-matching languages (e.g., swift/kotlin in a Python project), the entire project tree is scanned. In a 65k-file repo, this means ~8–9 complete directory scans adding seconds to startup.

Critically, this work has no observable effect: get_active_rules() at line 226 uses self._state.get(rule_id, RuleState()), and the default RuleState() has enabled=True (src/kimi_cli/rules/models.py:44). So rules without a state entry are already enabled by default. The auto-enable step merely writes explicit RuleState(enabled=True) entries for matching categories, which is indistinguishable from the default. Since auto_enable_by_path defaults to True (src/kimi_cli/config.py:183), every CLI startup pays this cost for zero behavioral change.

Prompt for agents
The _any_file_exists method at registry.py:183-194 is called from _detect_project_types (line 153) which is called from _auto_enable_rules (line 134). The entire auto-enable mechanism has no effect because get_active_rules (line 226) already defaults rules to enabled via RuleState() which has enabled=True.

Two possible fixes:
1. If the intent is for ALL rules to be enabled by default (current behavior), remove _auto_enable_rules, _detect_project_types, and _any_file_exists entirely. They add startup latency for no benefit.
2. If the intent is to ONLY enable rules matching the project type, change get_active_rules to treat missing state entries as disabled (not enabled). Then auto-enable would actually gate which rules are active. You'd also want to optimize _any_file_exists to avoid full recursive glob scans — e.g., use a single directory scan and collect all filenames, then check patterns against that set.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +355 to +359
logger.info(
"Loaded {count} active rules ({size} bytes)",
count=len(active_rules),
size=total_size,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Log reports total active rules count instead of actually injected count

In Runtime.create, the log at line 356 reports count=len(active_rules) — the total number of enabled rules from the registry. However, the loop above (lines 341–353) may include fewer rules in lines due to the max_rules limit (line 345) or the max_size byte limit (line 350). The logged count can be higher than the number of rules actually injected into the system prompt, making the log misleading for debugging prompt content.

Suggested change
logger.info(
"Loaded {count} active rules ({size} bytes)",
count=len(active_rules),
size=total_size,
)
active_rules_content = "".join(lines).strip()
logger.info(
"Injected {count}/{total} active rules ({size} bytes)",
count=len(lines),
total=len(active_rules),
size=total_size,
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Signed-off-by: T0M0R1N <70043311+Nemo4110@users.noreply.github.com>
@Nemo4110 Nemo4110 changed the title Three-tier Rules System feat: three-tier rules system Apr 8, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d9f553574

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# Enforce max_active_rules limit
if rule_count >= max_rules:
break
section = f"## {rule.name}\n\n{rule.content}\n\n"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor include_source when building injected rules

When rules.include_source is enabled in config, injected rule sections are still rendered as ## {rule.name} without any (level/id) annotation, so the new option has no runtime effect. This makes it impossible to tell which layer supplied a rule when overrides exist, even though the configuration advertises this behavior. Please branch the header format here based on config.rules.include_source.

Useful? React with 👍 / 👎.

Comment on lines +172 to +174
for lang, patterns in detection_patterns.items():
for pattern in patterns:
if await self._any_file_exists(pattern):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rescanning the entire tree for each detection pattern

Project-type auto-detection currently calls _any_file_exists() for every pattern, and each call performs a fresh recursive glob("**/*") over the whole workspace. With default auto_enable_by_path=true, startup can do dozens of full-tree traversals in large repos, causing noticeable latency before the first turn. This should be reduced to a single scan (or cached index) so enabling rules does not scale as O(patterns × files).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

@Nemo4110 Nemo4110 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Three-tier Rules System for Development Guidelines

1 participant