feat: always-inject tier for behavioral rules#53
Conversation
PreCompact hook was created but never registered in hooks.json. Now registered with 10s timeout. Hook writes discovery data to .agent/pre-compact-discovery.json for empirical testing of available input fields (transcript_path verification for FR-2).
Three-tier injection system: observations tagged with concept "always-inject" are now fetched independently of similarity matching and included in every session (session-start) and every prompt (user-prompt) context. Server changes: - GetAlwaysInjectObservations query (concepts @> GIN index) - GIN indexes on concepts, files_modified, files_read columns - Migration 048 for all new indexes - handleContextInject + handleSearchByPrompt return always_inject array - AlwaysInjectLimit (default 20) and ProjectInjectLimit (default 15) config Hook changes: - session-start.js renders <user-behavior-rules> block before <engram-context> - user-prompt.js merges always-inject + similarity-matched rules with dedup - Plugin version bumped to 0.6.0 Also adds GetObservationsByFile and GetPreviousObservationInSession queries for Phase 3 and Phase 4 (no callers yet).
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the context injection mechanism by introducing an 'always-inject' tier for behavioral rules, ensuring critical instructions are consistently present. It also improves database query performance for observation retrieval through new GIN indexes and provides greater control over context injection limits. These changes aim to make the system more reliable and efficient in delivering relevant information. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
ОбзорВ этом PR добавляется функция "always-inject" для извлечения и внедрения наблюдений на основе концепций и файлов. Реализация включает два новых параметра конфигурации, миграцию базы данных с индексами, три метода запроса к хранилищу, обновления обработчиков контекста и интеграцию с хуками плагина для управления поведением. Изменения
Оценка трудозатрат на рецензирование кода🎯 4 (Сложный) | ⏱️ ~45 минут Возможно связанные PR
Предлагаемые метки
Стихотворение
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.11.3)Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions Comment |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/config/config.go`:
- Around line 588-592: The config currently reads ENGRA M_PROJECT_INJECT_LIMIT
into cfg.ProjectInjectLimit but that field is unused; either remove the
ProjectInjectLimit field and its environment parsing (the block in config.go and
the struct field ProjectInjectLimit) or implement its enforcement where project
items are injected (e.g., in the code paths that enqueue or process
project-level injections — locate functions like the project injection/ingest or
enqueuing routines and apply a limit check against cfg.ProjectInjectLimit before
creating/injecting items), and update any related tests and configuration docs
accordingly.
In `@internal/db/gorm/observation_store.go`:
- Around line 488-505: GetObservationsByFile currently returns observations
across all projects for a given file path causing potential data leakage; update
the function signature (GetObservationsByFile) to accept a project identifier
(e.g., projectID string or uint), add a WHERE clause to the GORM query to filter
by the project column (e.g., "project_id = ?"), and propagate the new parameter
to all callers so only observations with the matching project_id are returned
while keeping the existing activeObservationFilter(), importanceOrdering(), and
JSONB file matching logic intact.
- Around line 491-496: GetObservationsByFile in ObservationStore builds fileJSON
via fmt.Sprintf which doesn't escape special characters; replace that with a
proper JSON marshal of a string array (e.g. json.Marshal([]string{filePath}))
and use the resulting bytes/string for fileJSON, handling the marshal error and
returning it if non-nil so the DB Where("files_modified @> ? OR files_read @>
?", fileJSON, fileJSON) receives valid JSON-escaped input.
- Around line 470-486: Change GetAlwaysInjectObservations to accept a project
string and apply projectScopeFilter(project) so results include project-scoped
and global observations only; specifically, update the function signature to
func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context,
project string, limit int) ([]*models.Observation, error) and add
Scopes(projectScopeFilter(project), activeObservationFilter(),
importanceOrdering()) to the query chain, then update all call sites that invoke
GetAlwaysInjectObservations to pass the current project value.
In `@plugin/engram/hooks/pre-compact.js`:
- Around line 37-45: The report path is built from __dirname so it ends up under
plugin/ rather than the repo root; change construction of logPath to join the
repository CWD from the hook context (e.g., ctx.CWD) with
'.agent/pre-compact-discovery.json' instead of using __dirname so the file is
written to the project root; update the code that creates dirs and writes
(fs.mkdirSync, fs.writeFileSync) to use this new logPath variable and keep the
same error logging behavior.
In `@plugin/engram/hooks/user-prompt.js`:
- Around line 86-96: The current dedup loop (handling allBehaviorRules ->
uniqueRules using seenRuleIds) skips deduping when rule.id is null, allowing
duplicates to be added; update the logic in the loop that walks allBehaviorRules
to apply a fallback deduplication key when rule.id is null by using a stable
identifier such as rule.title (or another unique stringified field) — e.g.
compute const key = (typeof rule.id === 'number' ? `id:${rule.id}` :
`title:${String(rule.title||'')}`) and track seen keys in seenRuleIds (or rename
to seenRuleKeys) so rules from alwaysInjectRules and behaviorRules without
numeric ids are still deduplicated before pushing to uniqueRules; ensure you
handle missing title gracefully (empty string) to avoid runtime errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c3f10c55-7bc3-498c-8747-6d0063e71ad9
📒 Files selected for processing (9)
internal/config/config.gointernal/db/gorm/migrations.gointernal/db/gorm/observation_store.gointernal/worker/handlers_context.goplugin/engram/.claude-plugin/plugin.jsonplugin/engram/hooks/hooks.jsonplugin/engram/hooks/pre-compact.jsplugin/engram/hooks/session-start.jsplugin/engram/hooks/user-prompt.js
| if v := strings.TrimSpace(os.Getenv("ENGRAM_PROJECT_INJECT_LIMIT")); v != "" { | ||
| if n, err := strconv.Atoi(v); err == nil && n > 0 { | ||
| cfg.ProjectInjectLimit = n | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Поиск использования ProjectInjectLimit в кодовой базе
rg -n "ProjectInjectLimit" --type=goRepository: thebtf/engram
Length of output: 385
Удалите или реализуйте использование неиспользуемого поля конфигурации.
Поле ProjectInjectLimit определено в структуре конфигурации и загружается из переменной окружения ENGRAM_PROJECT_INJECT_LIMIT, но по результатам поиска в кодовой базе оно не используется ни в каком коде. Либо удалите это поле, если оно больше не требуется, либо реализуйте его использование.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/config/config.go` around lines 588 - 592, The config currently reads
ENGRA M_PROJECT_INJECT_LIMIT into cfg.ProjectInjectLimit but that field is
unused; either remove the ProjectInjectLimit field and its environment parsing
(the block in config.go and the struct field ProjectInjectLimit) or implement
its enforcement where project items are injected (e.g., in the code paths that
enqueue or process project-level injections — locate functions like the project
injection/ingest or enqueuing routines and apply a limit check against
cfg.ProjectInjectLimit before creating/injecting items), and update any related
tests and configuration docs accordingly.
| // GetAlwaysInjectObservations retrieves observations tagged with the "always-inject" concept. | ||
| // These are unconditionally injected in every session regardless of similarity matching. | ||
| // Results are ordered by importance_score DESC, limited to the configured cap. | ||
| func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context, limit int) ([]*models.Observation, error) { | ||
| var dbObservations []Observation | ||
| err := s.db.WithContext(ctx). | ||
| Scopes(activeObservationFilter(), importanceOrdering()). | ||
| Where("concepts @> ?", `["always-inject"]`). | ||
| Limit(limit). | ||
| Find(&dbObservations).Error | ||
|
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return toModelObservations(dbObservations), nil | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find callers of GetAlwaysInjectObservations to verify project filtering intent
ast-grep --pattern 'GetAlwaysInjectObservations($$$)'
# Also check handler context for how this is used
rg -n -A 10 'GetAlwaysInjectObservations' --type=goRepository: thebtf/engram
Length of output: 3044
🏁 Script executed:
# Check ObservationStore struct and how it's initialized
rg -n "type ObservationStore struct" -A 5 --type=go
# Check how other methods in this file filter by project
rg -n "project" internal/db/gorm/observation_store.go --type=go | head -30
# Check activeObservationFilter implementation
rg -n "activeObservationFilter" -A 10 --type=goRepository: thebtf/engram
Length of output: 8586
🏁 Script executed:
# Check projectScopeFilter implementation
rg -n "projectScopeFilter" -A 8 --type=go
# Also check the handler context to see if project is available there
sed -n '460,475p' internal/worker/handlers_context.go
sed -n '1068,1085p' internal/worker/handlers_context.goRepository: thebtf/engram
Length of output: 4681
Добавьте фильтр проекта в GetAlwaysInjectObservations
Функция не фильтрует по project, что приводит к возврату наблюдений со статусом always-inject из ВСЕХ проектов, независимо от контекста вызова. Это нарушает изоляцию данных между проектами.
Сравните с аналогичными методами (GetActiveObservations, GetGuidanceObservations), которые принимают параметр project и используют projectScopeFilter(project) для включения как проектных, так и глобальных наблюдений в пределах одного проекта.
Измените сигнатуру:
func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context, project string, limit int) ([]*models.Observation, error) {
// Добавьте Scopes(projectScopeFilter(project), ...)
}Обновите обе вызывающие строки (handlers_context.go:464, 1072) для передачи доступного project.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/db/gorm/observation_store.go` around lines 470 - 486, Change
GetAlwaysInjectObservations to accept a project string and apply
projectScopeFilter(project) so results include project-scoped and global
observations only; specifically, update the function signature to func (s
*ObservationStore) GetAlwaysInjectObservations(ctx context.Context, project
string, limit int) ([]*models.Observation, error) and add
Scopes(projectScopeFilter(project), activeObservationFilter(),
importanceOrdering()) to the query chain, then update all call sites that invoke
GetAlwaysInjectObservations to pass the current project value.
| // GetObservationsByFile retrieves observations related to a specific file path. | ||
| // Matches against both files_modified and files_read JSONB arrays. | ||
| // Results are ordered by importance_score DESC. | ||
| func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) { | ||
| var dbObservations []Observation | ||
| fileJSON := fmt.Sprintf(`["%s"]`, filePath) | ||
| err := s.db.WithContext(ctx). | ||
| Scopes(activeObservationFilter(), importanceOrdering()). | ||
| Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON). | ||
| Limit(limit). | ||
| Find(&dbObservations).Error | ||
|
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return toModelObservations(dbObservations), nil | ||
| } |
There was a problem hiding this comment.
Отсутствует фильтрация по проекту — возможная утечка данных.
Функция GetObservationsByFile не принимает параметр project и не фильтрует результаты по проекту. Это означает, что запрос вернёт наблюдения из ВСЕХ проектов, которые ссылаются на указанный путь к файлу.
Учитывая, что пути к файлам могут совпадать между проектами (например, README.md, main.go), это может привести к нежелательной утечке информации между проектами.
♻️ Предлагаемое исправление с добавлением фильтра по проекту
-func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
+func (s *ObservationStore) GetObservationsByFile(ctx context.Context, project, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
- fileJSON := fmt.Sprintf(`["%s"]`, filePath)
- err := s.db.WithContext(ctx).
- Scopes(activeObservationFilter(), importanceOrdering()).
+ fileJSONBytes, _ := json.Marshal([]string{filePath})
+ fileJSON := string(fileJSONBytes)
+ err := s.db.WithContext(ctx).
+ Scopes(projectScopeFilter(project), activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/db/gorm/observation_store.go` around lines 488 - 505,
GetObservationsByFile currently returns observations across all projects for a
given file path causing potential data leakage; update the function signature
(GetObservationsByFile) to accept a project identifier (e.g., projectID string
or uint), add a WHERE clause to the GORM query to filter by the project column
(e.g., "project_id = ?"), and propagate the new parameter to all callers so only
observations with the matching project_id are returned while keeping the
existing activeObservationFilter(), importanceOrdering(), and JSONB file
matching logic intact.
| func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) { | ||
| var dbObservations []Observation | ||
| fileJSON := fmt.Sprintf(`["%s"]`, filePath) | ||
| err := s.db.WithContext(ctx). | ||
| Scopes(activeObservationFilter(), importanceOrdering()). | ||
| Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON). |
There was a problem hiding this comment.
Критическая ошибка: отсутствует экранирование JSON для filePath.
Использование fmt.Sprintf для построения JSON-строки небезопасно. Если filePath содержит специальные символы (", \, управляющие символы), результирующий JSON будет невалидным, что приведёт к ошибке запроса или некорректному поведению.
🐛 Предлагаемое исправление с использованием json.Marshal
func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
- fileJSON := fmt.Sprintf(`["%s"]`, filePath)
+ fileJSONBytes, err := json.Marshal([]string{filePath})
+ if err != nil {
+ return nil, fmt.Errorf("marshal file path: %w", err)
+ }
+ fileJSON := string(fileJSONBytes)
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) { | |
| var dbObservations []Observation | |
| fileJSON := fmt.Sprintf(`["%s"]`, filePath) | |
| err := s.db.WithContext(ctx). | |
| Scopes(activeObservationFilter(), importanceOrdering()). | |
| Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON). | |
| func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) { | |
| var dbObservations []Observation | |
| fileJSONBytes, marshalErr := json.Marshal([]string{filePath}) | |
| if marshalErr != nil { | |
| return nil, fmt.Errorf("marshal file path: %w", marshalErr) | |
| } | |
| fileJSON := string(fileJSONBytes) | |
| err := s.db.WithContext(ctx). | |
| Scopes(activeObservationFilter(), importanceOrdering()). | |
| Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON). |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/db/gorm/observation_store.go` around lines 491 - 496,
GetObservationsByFile in ObservationStore builds fileJSON via fmt.Sprintf which
doesn't escape special characters; replace that with a proper JSON marshal of a
string array (e.g. json.Marshal([]string{filePath})) and use the resulting
bytes/string for fileJSON, handling the marshal error and returning it if
non-nil so the DB Where("files_modified @> ? OR files_read @> ?", fileJSON,
fileJSON) receives valid JSON-escaped input.
| // Write to file so agent can read it | ||
| const logPath = path.join(__dirname, '..', '..', '..', '.agent', 'pre-compact-discovery.json'); | ||
| try { | ||
| fs.mkdirSync(path.dirname(logPath), { recursive: true }); | ||
| fs.writeFileSync(logPath, JSON.stringify(report, null, 2)); | ||
| console.error(`[pre-compact] Discovery written to ${logPath}`); | ||
| } catch (e) { | ||
| console.error(`[pre-compact] Failed to write log: ${e.message}`); | ||
| } |
There was a problem hiding this comment.
Путь к файлу отчёта может не соответствовать ожиданиям.
logPath вычисляется как path.join(__dirname, '..', '..', '..', '.agent', ...), что разрешается относительно директории хука (plugin/engram/hooks/). Результат: plugin/.agent/pre-compact-discovery.json, а не корень проекта.
Если .agent/ должен быть в корне проекта пользователя, рассмотрите использование ctx.CWD или аналогичного контекстного пути.
🔧 Предложение: использовать CWD из контекста
- const logPath = path.join(__dirname, '..', '..', '..', '.agent', 'pre-compact-discovery.json');
+ // Если CWD доступен в контексте, использовать его как базу
+ const projectRoot = ctx && ctx.CWD ? ctx.CWD : path.join(__dirname, '..', '..', '..');
+ const logPath = path.join(projectRoot, '.agent', 'pre-compact-discovery.json');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugin/engram/hooks/pre-compact.js` around lines 37 - 45, The report path is
built from __dirname so it ends up under plugin/ rather than the repo root;
change construction of logPath to join the repository CWD from the hook context
(e.g., ctx.CWD) with '.agent/pre-compact-discovery.json' instead of using
__dirname so the file is written to the project root; update the code that
creates dirs and writes (fs.mkdirSync, fs.writeFileSync) to use this new logPath
variable and keep the same error logging behavior.
| // Merge always-inject + similarity-matched behavioral rules into one block | ||
| const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules]; | ||
| // Deduplicate by ID (always-inject may overlap with similarity results) | ||
| const seenRuleIds = new Set(); | ||
| const uniqueRules = []; | ||
| for (const rule of allBehaviorRules) { | ||
| const ruleId = rule && typeof rule.id === 'number' ? rule.id : null; | ||
| if (ruleId !== null && seenRuleIds.has(ruleId)) continue; | ||
| if (ruleId !== null) seenRuleIds.add(ruleId); | ||
| uniqueRules.push(rule); | ||
| } |
There was a problem hiding this comment.
Дедупликация не защищает от дубликатов с отсутствующим ID.
Если rule.id равен null или отсутствует, правило всё равно добавляется в uniqueRules без проверки на дубликаты. Это может привести к повторному включению одного и того же правила, если оно присутствует и в alwaysInjectRules, и в behaviorRules без валидного ID.
🛡️ Предложение: добавить fallback-дедупликацию по title
const seenRuleIds = new Set();
+ const seenTitles = new Set();
const uniqueRules = [];
for (const rule of allBehaviorRules) {
const ruleId = rule && typeof rule.id === 'number' ? rule.id : null;
if (ruleId !== null && seenRuleIds.has(ruleId)) continue;
if (ruleId !== null) seenRuleIds.add(ruleId);
+ // Fallback dedup by title when ID is missing
+ if (ruleId === null && rule && rule.title) {
+ if (seenTitles.has(rule.title)) continue;
+ seenTitles.add(rule.title);
+ }
uniqueRules.push(rule);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Merge always-inject + similarity-matched behavioral rules into one block | |
| const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules]; | |
| // Deduplicate by ID (always-inject may overlap with similarity results) | |
| const seenRuleIds = new Set(); | |
| const uniqueRules = []; | |
| for (const rule of allBehaviorRules) { | |
| const ruleId = rule && typeof rule.id === 'number' ? rule.id : null; | |
| if (ruleId !== null && seenRuleIds.has(ruleId)) continue; | |
| if (ruleId !== null) seenRuleIds.add(ruleId); | |
| uniqueRules.push(rule); | |
| } | |
| // Merge always-inject + similarity-matched behavioral rules into one block | |
| const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules]; | |
| // Deduplicate by ID (always-inject may overlap with similarity results) | |
| const seenRuleIds = new Set(); | |
| const seenTitles = new Set(); | |
| const uniqueRules = []; | |
| for (const rule of allBehaviorRules) { | |
| const ruleId = rule && typeof rule.id === 'number' ? rule.id : null; | |
| if (ruleId !== null && seenRuleIds.has(ruleId)) continue; | |
| if (ruleId !== null) seenRuleIds.add(ruleId); | |
| // Fallback dedup by title when ID is missing | |
| if (ruleId === null && rule && rule.title) { | |
| if (seenTitles.has(rule.title)) continue; | |
| seenTitles.add(rule.title); | |
| } | |
| uniqueRules.push(rule); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugin/engram/hooks/user-prompt.js` around lines 86 - 96, The current dedup
loop (handling allBehaviorRules -> uniqueRules using seenRuleIds) skips deduping
when rule.id is null, allowing duplicates to be added; update the logic in the
loop that walks allBehaviorRules to apply a fallback deduplication key when
rule.id is null by using a stable identifier such as rule.title (or another
unique stringified field) — e.g. compute const key = (typeof rule.id ===
'number' ? `id:${rule.id}` : `title:${String(rule.title||'')}`) and track seen
keys in seenRuleIds (or rename to seenRuleKeys) so rules from alwaysInjectRules
and behaviorRules without numeric ids are still deduplicated before pushing to
uniqueRules; ensure you handle missing title gracefully (empty string) to avoid
runtime errors.
Summary
always-injectconcept injected unconditionally in every sessionENGRAM_ALWAYS_INJECT_LIMIT(default 20),ENGRAM_PROJECT_INJECT_LIMIT(default 15)<user-behavior-rules>block BEFORE context/memory blocksTest plan
always-injectconcept via MCP tools<user-behavior-rules>appearsSummary by CodeRabbit
Новые функции
Улучшения