feat: keyword-triggered belief categories (v1: soft injection) - #1127
Conversation
…fig gate New src/aelfrice/category.py for the keyword-triggered belief-categories feature (#1126): Category + CategoryTrigger dataclasses, stable trigger_json (de)serialization, deterministic keyword (word-boundary, case-insensitive, whitespace-tolerant) and fnmatch glob matchers, name/ default_lock validation, the default-off is_enabled() config gate (mirrors sentiment_feedback), and the 5-category SEED_CATEGORIES starter set. Pure: no store, no disk, no clock, no embeddings (#605-clean). 35 unit tests; pyright-strict clean.
Two additive tables in _SCHEMA (#1126): categories (name PK, always_on, trigger_json, default_lock, created_at) and the belief_categories M2M join (modeled on belief_documents, FK-CASCADE both sides). Additive CREATE TABLE only — passes the destructive-only migration-policy gate and is present on every fresh store. Store methods: upsert_category (idempotent, preserves created_at), get/list_categories, set_category_trigger, delete_category, assign/unassign_belief_to_category (idempotent, FK-validated), get_categories_for_belief, get_beliefs_for_category (active-only, excludes retired). _row_to_category rehydrates via CategoryTrigger.from_json. 9 store tests; no net-new pyright errors on the legacy store module.
Adds the visible `aelf category` verb (#1126) with nested actions: init (seed the 5 starter categories idempotently), add, list, show, set-trigger, assign, unassign, delete. Adds repeatable `--category NAME` to `aelf lock` (assigns the locked belief; missing category is reported+skipped, non-fatal). New slash_commands/category.md and category registered in the slash-command contract test's EXPECTED_COMMANDS. Trigger config plumbed via _trigger_from_args. 9 CLI tests; slash-command suite green; net-zero pyright on cli.py.
…ubmit Wires the belief-categories feature (#1126) into user_prompt_submit: _maybe_category_injection_block emits a <belief-category-rules> block ahead of the retrieval body (mirroring <cadence-checkpoint>) when the lane is enabled and a category is always_on or has a keyword phrase in the prompt. Member rules are de-duplicated by belief id across fired categories and bounded by CATEGORY_BLOCK_CHAR_BUDGET (truncates to a manifest line). Runs independent of the gate-skip/BM25 path so a triggered rule fires even on shape-gated prompts. Advisory only — never blocks a tool call (v1 non-goal). Default-off (AELFRICE_BELIEF_CATEGORIES / [belief_categories] enabled), fail-soft (any error returns ""), deterministic (name-ASC categories, stable member order, no embeddings). 9 tests; net-zero pyright on hook.py.
Documents the belief-categories feature (#1126): [belief_categories] config section (default-off enabled flag) in CONFIG.md, the aelf category verb in COMMANDS.md, /aelf:category in SLASH_COMMANDS.md (file count 29 -> 30), a design memo at docs/design/belief_categories.md (problem, prior- art #199 constraint, data model, matching, injection lane, non-goals, follow-ups), and an [Unreleased] Added entry in CHANGELOG/v4.md.
Reviewer's GuideImplements v1 of keyword-triggered belief categories as an advisory UserPromptSubmit injection lane: introduces a pure Sequence diagram for UserPromptSubmit belief-category injectionsequenceDiagram
actor User
participant Hook as user_prompt_submit
participant CatMod as category
participant Store
User->>Hook: send prompt
Hook->>Hook: _maybe_category_injection_block(prompt, payload_cwd, stderr)
Hook->>CatMod: is_enabled(toml_cfg)
CatMod-->>Hook: enabled?
alt enabled
Hook->>Store: list_categories()
Store-->>Hook: categories
Hook->>CatMod: match_prompt(prompt, categories)
CatMod-->>Hook: fired_categories
alt any fired_categories
loop for each fired category
Hook->>Store: get_beliefs_for_category(cat.name)
Store-->>Hook: member_beliefs
Hook->>Hook: build rules block (dedupe, budget)
end
Hook-->>Hook: return <belief-category-rules> block
Hook->>User: write block before retrieval body
else no fired_categories
Hook-->>Hook: return ""
end
else not enabled
Hook-->>Hook: return ""
end
Entity relationship diagram for belief categories data modelerDiagram
beliefs {
TEXT id PK
TEXT valid_to
}
categories {
TEXT name PK
INTEGER always_on
TEXT trigger_json
TEXT default_lock
TEXT created_at
}
belief_categories {
TEXT belief_id FK
TEXT category_name FK
TEXT created_at
}
beliefs ||--o{ belief_categories : has
categories ||--o{ belief_categories : groups
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Review limit reached
Next review available in: 26 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds keyword-triggered belief categories with deterministic matching, SQLite-backed memberships, CLI management, default-off configuration, and fail-soft ChangesBelief category model
Category persistence and membership
Category CLI and command surface
Prompt reranking and configuration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR-size soft capThis PR is over the advisory size threshold:
Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the |
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/aelfrice/hook.py" line_range="2040" />
<code_context>
+# Per-category character budget for the injected block. Small on purpose
+# (~a few hundred tokens) so a fired category — or several — cannot crowd
+# out the retrieval surface. Overflow is truncated to a manifest line.
+CATEGORY_BLOCK_CHAR_BUDGET: Final[int] = 1600
+
+
</code_context>
<issue_to_address>
**nitpick:** The category block budget logic doesn’t match the comment and ignores header/boilerplate length.
The implementation uses a single `used` counter across all categories and only counts rule lines, excluding headers, separators, and the explanatory note. As a result, the effective cap is higher than stated and not truly per-category. Please either (a) enforce the budget against the full block length (including headers and note) or (b) update the comment to describe this as an approximate, shared budget applied only to rule text.
</issue_to_address>
### Comment 2
<location path="src/aelfrice/category.py" line_range="239-244" />
<code_context>
+ return bool(pat and pat.search(prompt))
+
+
+def _glob_hit(candidates: tuple[str, ...], values: list[str]) -> bool:
+ """True when any value matches any ``fnmatch`` glob (case-insensitive
+ via ``fnmatch.fnmatch``'s normcase on the pattern)."""
+ for value in values:
+ for pattern in candidates:
+ if pattern and fnmatch.fnmatch(value, pattern):
+ return True
+ return False
</code_context>
<issue_to_address>
**issue (bug_risk):** The glob matching is case-sensitive on POSIX despite the docstring claiming case-insensitive behavior.
On POSIX, `os.path.normcase` is a no-op, so `fnmatch.fnmatch` does **not** make matching case-insensitive despite the docstring. This mismatch can lead to missed triggers when users expect case-insensitive globs. Either implement explicit case-insensitive matching (e.g., lowercase both `value` and `pattern` and use `fnmatch.fnmatchcase`) or update the docstring to state that matching is case-sensitive and follows the platform’s filesystem semantics.
</issue_to_address>
### Comment 3
<location path="src/aelfrice/cli.py" line_range="2194-2201" />
<code_context>
+ print(f" {belief.id} {snippet}", file=w)
+ return 0
+
+ if action == "set-trigger":
+ name = cast("str", args.name)
+ if store.get_category(name) is None:
+ print(f"aelf category: no such category: {name}", file=w)
+ return 1
+ store.set_category_trigger(name, _trigger_from_args(args).to_json())
+ print(f"category: {name} trigger updated", file=w)
+ return 0
+
</code_context>
<issue_to_address>
**suggestion:** The `set-trigger` path doesn’t handle the race where the category disappears between the existence check and update.
`set_category_trigger` already returns a `bool` indicating whether a row was updated. If the category is deleted between `get_category` and `set_category_trigger`, you’ll still print `trigger updated` even though nothing changed. Consider dropping the pre-check and basing the message entirely on the boolean result (printing `no such category` when it’s `False`).
```suggestion
if action == "set-trigger":
name = cast("str", args.name)
updated = store.set_category_trigger(name, _trigger_from_args(args).to_json())
if updated:
print(f"category: {name} trigger updated", file=w)
return 0
else:
print(f"aelf category: no such category: {name}", file=w)
return 1
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
merge-train: blocked 3 review thread(s) are unresolved on these files: src/aelfrice/category.py, src/aelfrice/cli.py, src/aelfrice/hook.py. Resolve them on the PR (click 'Resolve conversation' on each) and re-add the label. The |
…et-trigger TOCTOU Addresses three review findings on #1126: - category._glob_hit: fnmatch.fnmatch case-folds via os.path.normcase, a no-op on POSIX — so globs were case-sensitive on Linux (CI) and insensitive on macOS, breaking the #605 determinism contract. Lower- case both sides and use fnmatchcase for platform-independent matching. - hook category block: count category headers against the shared char budget (not just rule text) and document it honestly as a single shared budget over rules+headers, with the fixed wrapper as bounded overhead. - cli set-trigger: drop the get_category pre-check and branch on set_category_trigger's rowcount bool — removes a delete-in-between TOCTOU that could print 'trigger updated' on a no-op.
… block R&D on the v1 design (documented in docs/design/belief_categories.md) refuted the separate-block approach: - R1: a locked rule in a fired keyword category injected TWICE (category block + L0 retrieval block) — all seed categories default locked. - R2: across fresh/aged beliefs, tiny/45k stores, budget 200-2400, a category member was ALWAYS already in the retrieval output; the block never surfaced net-new content, only duplicated. - R3: literal keyword matching missed ~39% of natural phrasings. Redesign: _apply_category_boost reranks the single retrieval output so a fired category's members lead the <aelfrice-memory> block, pulls in a bounded set (CATEGORY_BOOST_MAX_EXTRA=8) of members retrieval missed, and prepends a <category-focus> label. One injection, no duplicate block; deterministic (name-ASC categories, stable member order, id-dedup); fail-soft (hits pass through unchanged on disable/no-fire/error). Also: expanded git-workflow seed keywords (committed/merged/ship/land/ pr/github/…) cutting the R3 miss rate 39% -> 17% (the residual is the deterministic-literal-matching floor). Docs + CHANGELOG updated to describe rerank-on-trigger and the R&D rationale. Hook tests rewritten for rerank semantics (promote-to-top, no-dup, bounded surfacing, label, default-off, determinism). hook.py net-zero pyright; category.py strict-clean.
Design changed after R&D — now rerank-on-trigger, not a separate blockRan empirical R&D on the original design (separate
New mechanism: Also folded in three review fixes from the first pass (cross-platform glob case-folding, budget accounting, set-trigger TOCTOU). Full suite green (5935 passed); hook.py net-zero pyright. |
…inter
Use a full-word conditional noun ('category'/'categories') in the
<category-focus> label; the f-string split 'categor{...}' was flagged by
the typos CI check as a misspelling of 'category'. No behavior change.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/aelfrice/cli.py`:
- Around line 2110-2126: Update the category init flow in the action == "init"
branch to preserve existing user-customized categories: check whether each seed
category already exists before calling store.upsert_category, and only insert
missing seeds. Keep reporting each seed category and the existing initialization
behavior for newly added categories without overwriting stored always_on,
trigger_json, or default_lock values.
In `@src/aelfrice/hook.py`:
- Around line 973-983: Update _apply_category_boost so members newly fetched
through store.get_beliefs_for_category(cat.name) are rechecked with
_filter_by_project_context and _filter_session_exclusions before being appended
or injected into hits. Preserve the existing reuse of already-filtered objects
in the existing = hit_by_id.get(member.id) path, and keep excluded or
out-of-context members out of category_focus and the retrieval output.
In `@src/aelfrice/store.py`:
- Around line 3458-3477: Make assign_belief_to_category derive missing-belief or
missing-category errors from the atomic INSERT rather than relying on the
separate get_belief/get_category checks. Preserve idempotent insertion and
translate foreign-key failures, including concurrent deletions, into the
documented ValueError messages expected by the CLI; keep the operation
transactional and avoid introducing non-atomic writes.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f10a11bd-d1ca-4b1c-b2d9-a26b69b52a19
📒 Files selected for processing (15)
CHANGELOG/v4.mddocs/design/belief_categories.mddocs/user/COMMANDS.mddocs/user/CONFIG.mddocs/user/SLASH_COMMANDS.mdsrc/aelfrice/category.pysrc/aelfrice/cli.pysrc/aelfrice/hook.pysrc/aelfrice/slash_commands/category.mdsrc/aelfrice/store.pytests/test_belief_categories_store.pytests/test_category_module.pytests/test_cli_category.pytests/test_hook_category_injection.pytests/test_slash_commands.py
|
merge-train: blocked 3 review thread(s) are unresolved on these files: src/aelfrice/cli.py, src/aelfrice/hook.py, src/aelfrice/store.py. Resolve them on the PR (click 'Resolve conversation' on each) and re-add the label. The |
…ilters surfaced members, assign TOCTOU Three CodeRabbit findings on #1127: - category init: skip existing categories on re-run instead of upserting, so a customized seed (e.g. expanded git-workflow keywords) survives an init re-run / upgrade. init is additive, not a reset. - _apply_category_boost: surfaced retrieval-missed members bypassed the project-context and session-exclusion filters that the retrieval hits already passed — they could leak a foreign-project or scoped-out belief. Run extras through _filter_by_project_context + _filter_session_exclusions (session_id now threaded in) before injecting. - assign_belief_to_category: INSERT OR IGNORE silently drops FK violations, so a belief/category deleted between the existence check and the write would report a phantom success. Verify the row landed; raise on the race. 2 regression tests (init preserves customization; surfaced member respects project context). Net-zero pyright on all three touched modules.
|
merge-train: merged 5e7e52f → |
What
Ships v1 of keyword-triggered belief categories (umbrella #1126): group beliefs into named categories and bind each to an activation trigger, so a category's rules surface into context when they're relevant — the conditional complement to a static
CLAUDE.md/AGENTS.md.Closes #1126.
Design (per the umbrella spec + the /aelf:wonder research)
Advisory injection, not enforcement. Per the prior enforcement-triad decision (#199) and the fail-open/never-deny hook contract, v1 surfaces rules — it never blocks a tool call. A hard-block lane is an explicit non-goal (see the design memo). This was a deliberate push-back on the original "hook that blocks on keywords" framing.
Layers (5 atomic commits)
aelfrice.category— pure module:Category/CategoryTrigger, stabletrigger_json(de)serialization, deterministic keyword (case-insensitive, word-boundary, whitespace-tolerant) +fnmatchglob matchers,is_enableddefault-off gate, 5-categorySEED_CATEGORIES. No store/disk/clock/embeddings (v3.0 PHILOSOPHY: natural-language-relatedness gate — deterministic vs embedding #605-clean).categories+belief_categoriesM2M, modeled onbelief_documents, FK-CASCADE); CRUD + membership methods. Additive DDL only → passes the destructive-only migration-policy gate; noBeliefcolumn.aelf categoryverb (init/add/list/show/set-trigger/assign/unassign/delete),aelf lock --category,/aelf:categoryslash file, registered inEXPECTED_COMMANDS.UserPromptSubmitlane: emits a<belief-category-rules>block ahead of the retrieval body (mirrors<cadence-checkpoint>), independent of the shape-gate, dedup by belief id, bounded by a char budget, fail-soft.[belief_categories]in CONFIG.md,categoryin COMMANDS.md/SLASH_COMMANDS.md, design memo, CHANGELOG[Unreleased].Scope notes
always_on) is wired into the hook in v1. Thetool_globs/file_globslanes are parsed, stored, and matched (unit-tested), but theirPreToolUsewiring is a documented follow-up.Tests
New:
test_category_module.py(35),test_belief_categories_store.py(9),test_cli_category.py(9),test_hook_category_injection.py(9). Full suite: 5934 passed, 69 skipped, 75 xfailed. No net-new pyright errors on the touched legacy modules; the new pure module is pyright-strict clean.Summary by Sourcery
Introduce keyword-triggered belief categories with advisory hook injection and CLI management.
New Features:
aelfrice.categorymodule for keyword/glob-based activation triggers.aelf categoryCLI and/aelf:categoryslash command to manage categories and membership, including seeding starter categories and assigning at lock time.<belief-category-rules>block when categories fire.Enhancements:
Documentation:
[belief_categories]config section, COMMANDS/SLASH_COMMANDS entries, and a design memo.Tests:
Summary by CodeRabbit
New Features
categorycommand and/aelf:categoryslash command for creating and managing categories.Documentation