Skip to content

feat: keyword-triggered belief categories (v1: soft injection) - #1127

Merged
github-actions[bot] merged 9 commits into
mainfrom
feat/issue-1126-belief-categories
Jul 15, 2026
Merged

feat: keyword-triggered belief categories (v1: soft injection)#1127
github-actions[bot] merged 9 commits into
mainfrom
feat/issue-1126-belief-categories

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 15, 2026

Copy link
Copy Markdown
Owner

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)

  1. aelfrice.category — pure module: Category/CategoryTrigger, stable trigger_json (de)serialization, deterministic keyword (case-insensitive, word-boundary, whitespace-tolerant) + fnmatch glob matchers, is_enabled default-off gate, 5-category SEED_CATEGORIES. No store/disk/clock/embeddings (v3.0 PHILOSOPHY: natural-language-relatedness gate — deterministic vs embedding #605-clean).
  2. store — two additive tables (categories + belief_categories M2M, modeled on belief_documents, FK-CASCADE); CRUD + membership methods. Additive DDL only → passes the destructive-only migration-policy gate; no Belief column.
  3. CLI — visible aelf category verb (init/add/list/show/set-trigger/assign/unassign/delete), aelf lock --category, /aelf:category slash file, registered in EXPECTED_COMMANDS.
  4. hook — default-off UserPromptSubmit lane: 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.
  5. docs[belief_categories] in CONFIG.md, category in COMMANDS.md/SLASH_COMMANDS.md, design memo, CHANGELOG [Unreleased].

Scope notes

  • Only the prompt keyword lane (+ always_on) is wired into the hook in v1. The tool_globs/file_globs lanes are parsed, stored, and matched (unit-tested), but their PreToolUse wiring is a documented follow-up.
  • No auto-classification — membership is user-driven.

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:

  • Add belief categories data model and pure aelfrice.category module for keyword/glob-based activation triggers.
  • Expose aelf category CLI and /aelf:category slash command to manage categories and membership, including seeding starter categories and assigning at lock time.
  • Enable an optional UserPromptSubmit hook lane that injects relevant belief-category rules into context as a <belief-category-rules> block when categories fire.

Enhancements:

  • Extend the store with additive tables and CRUD APIs to persist categories and their belief memberships without altering the core Belief schema.

Documentation:

  • Document belief categories configuration, CLI usage, and design, including new [belief_categories] config section, COMMANDS/SLASH_COMMANDS entries, and a design memo.

Tests:

  • Add unit tests for the category module, store integration, CLI surface, hook injection behavior, and slash command registration.

Summary by CodeRabbit

  • New Features

    • Added keyword-triggered belief categories for organizing and surfacing relevant rules.
    • Category matches can reorder retrieved results and display a concise category focus indicator.
    • Added the category command and /aelf:category slash command for creating and managing categories.
    • Beliefs can be assigned to one or more categories when locked or through category management commands.
    • Feature is advisory, fail-soft, and disabled by default; it can be enabled through configuration.
  • Documentation

    • Added configuration, command reference, and design documentation for belief categories.

…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.
@robotrocketscience robotrocketscience added the author-Setr PR coordination mutex label Jul 15, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements v1 of keyword-triggered belief categories as an advisory UserPromptSubmit injection lane: introduces a pure aelfrice.category module for deterministic triggers and config gating, adds two additive store tables plus CRUD/membership APIs, wires a new aelf category CLI (and /aelf:category slash) including aelf lock --category, and surfaces fired categories’ member rules in a bounded <belief-category-rules> block controlled by a new [belief_categories] config section, with full test and docs coverage.

Sequence diagram for UserPromptSubmit belief-category injection

sequenceDiagram
  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
Loading

Entity relationship diagram for belief categories data model

erDiagram
  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
Loading

File-Level Changes

Change Details Files
New pure belief-category module providing category model, trigger (de)serialization, deterministic keyword/glob matchers, config gate, and seeded starter categories.
  • Define CategoryTrigger with stable JSON trigger_json serialization, tolerant from_json, and helper to detect empty triggers.
  • Define Category dataclass plus normalize_name and normalize_default_lock validation helpers and constants for lock hints.
  • Implement deterministic keyword, tool-glob, and file-glob matchers (keyword_hit, command_hit, paths_hit) and match_prompt to select fired categories in name-ASC order.
  • Add is_enabled config gate reading AELFRICE_BELIEF_CATEGORIES env var and [belief_categories] enabled from TOML-like config dict.
  • Define SEED_CATEGORIES 5-category starter set encoding the initial taxonomy and triggers.
src/aelfrice/category.py
Extend store schema and MemoryStore API to persist categories and M2M belief membership with deterministic queries and FK-cascade semantics.
  • Add additive categories and belief_categories tables plus index to _SCHEMA, modeled on belief_documents, with trigger_json blob and default_lock hint.
  • Introduce _row_to_category helper converting a categories row into a Category, using CategoryTrigger.from_json for tolerant parsing.
  • Add MemoryStore methods for category CRUD (upsert_category, get_category, list_categories, set_category_trigger, delete_category) using immediate commits and preserving created_at on upsert.
  • Add membership APIs (assign_belief_to_category, unassign_belief_from_category, get_categories_for_belief, get_beliefs_for_category) with FK existence checks, ON DELETE CASCADE reliance, and exclusion of retired beliefs from injection queries.
  • Use TYPE_CHECKING-time import of Category to satisfy pyright without introducing runtime import cycles.
src/aelfrice/store.py
Add CLI surface for managing belief categories and assigning beliefs at lock time, plus a new slash command wrapper.
  • Extend aelf lock with --category NAME (repeatable) to assign the locked belief to existing categories via store.assign_belief_to_category, skipping missing categories non-fatally.
  • Introduce _category_lock_choices and _trigger_from_args helpers to share lock-value choices and trigger construction logic across subcommands.
  • Implement _cmd_category dispatcher handling init/add/list/show/set-trigger/assign/unassign/delete actions using the store + aelfrice.category helpers, with user-facing error messages and deterministic listing formats.
  • Wire new category subparser into build_parser with appropriate arguments for triggers and default-lock choices, and register func=_cmd_category.
  • Add /aelf:category slash-command markdown describing how to invoke the CLI via uv run aelf category … and documenting the advisory, default-off nature of the lane.
src/aelfrice/cli.py
src/aelfrice/slash_commands/category.md
docs/user/SLASH_COMMANDS.md
tests/test_cli_category.py
tests/test_slash_commands.py
Wire a default-off belief-category injection lane into the UserPromptSubmit hook, emitting a bounded advisory rules block when categories fire.
  • Add _maybe_category_injection_block helper that checks category.is_enabled, loads categories from the store, calls match_prompt, collects de-duplicated member beliefs, and formats a <belief-category-rules> block with explanatory note and optional truncation tail.
  • Introduce CATEGORY_BLOCK_CHAR_BUDGET constant and truncation logic to cap per-turn injected characters and append a manifest when truncated.
  • Call _maybe_category_injection_block from user_prompt_submit after cadence checkpoint emission but before retrieval body, independent of the prompt-shape gate, and write the block when non-empty.
  • Implement fail-soft behavior: catch all exceptions in _maybe_category_injection_block, log a non-fatal message to stderr, and return an empty string.
  • Add tests covering the block builder and end-to-end hook behavior, including enable/disable gating, keyword vs always-on firing, deduplication, determinism, truncation, and preservation of exit-0 semantics.
src/aelfrice/hook.py
tests/test_hook_category_injection.py
Document the belief-categories feature, config knobs, commands, and changelog entry, and add focused unit tests for the pure module and store layer.
  • Extend CONFIG.md with [belief_categories] section explaining the feature, the enabled knob, precedence between env and TOML, and operational behavior of the injection lane.
  • Update COMMANDS.md and SLASH_COMMANDS.md to describe aelf category / /aelf:category, its actions, and the advisory default-off nature, and adjust counts of slash command files.
  • Add docs/design/belief_categories.md design memo detailing problem, constraints, v1 design, non-goals, and follow-ups.
  • Add CHANGELOG v4 entry summarizing the feature, schema additions, module, CLI, config gate, and non-goals.
  • Create test_category_module.py for the pure module and test_belief_categories_store.py for store-level CRUD/membership semantics, including FK cascade and retired-belief filtering.
docs/user/CONFIG.md
docs/user/COMMANDS.md
docs/user/SLASH_COMMANDS.md
CHANGELOG/v4.md
docs/design/belief_categories.md
tests/test_category_module.py
tests/test_belief_categories_store.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1126 Introduce a belief category data model and CRUD/assignment surfaces: additive categories and belief_categories tables, store methods, CLI (aelf category subcommands, aelf lock --category), and a /aelf:category slash command.
#1126 Implement a default-off, deterministic, fail-soft UserPromptSubmit injection lane that surfaces relevant belief-category members as an advisory <belief-category-rules> block (always-on + keyword-triggered), without changing behavior when disabled.
#1126 Document the belief-categories feature: configuration ([belief_categories] in CONFIG), command surface (COMMANDS/SLASH_COMMANDS), and a design memo describing scope, non-goals, and behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@robotrocketscience, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16d6fb14-8628-4a38-a201-f4d96fcc8cd3

📥 Commits

Reviewing files that changed from the base of the PR and between 15f9224 and 5e7e52f.

📒 Files selected for processing (5)
  • src/aelfrice/cli.py
  • src/aelfrice/hook.py
  • src/aelfrice/store.py
  • tests/test_cli_category.py
  • tests/test_hook_category_injection.py
📝 Walkthrough

Walkthrough

Adds keyword-triggered belief categories with deterministic matching, SQLite-backed memberships, CLI management, default-off configuration, and fail-soft UserPromptSubmit reranking that labels active categories and surfaces bounded members.

Changes

Belief category model

Layer / File(s) Summary
Category model and matching
docs/design/belief_categories.md, src/aelfrice/category.py, tests/test_category_module.py
Defines category and trigger data structures, tolerant JSON handling, deterministic keyword/glob matching, enablement precedence, seed categories, and unit coverage.

Category persistence and membership

Layer / File(s) Summary
Category persistence and membership
src/aelfrice/store.py, tests/test_belief_categories_store.py
Adds category and many-to-many membership tables, store APIs, validation, cascading deletes, deterministic ordering, and active-belief filtering.

Category CLI and command surface

Layer / File(s) Summary
Category CLI and command surface
src/aelfrice/cli.py, src/aelfrice/slash_commands/category.md, tests/test_cli_category.py, tests/test_slash_commands.py, docs/user/SLASH_COMMANDS.md
Adds category administration commands, repeatable lock-time assignment, slash-command documentation, and CLI/surface consistency tests.

Prompt reranking and configuration

Layer / File(s) Summary
Prompt reranking and configuration
src/aelfrice/hook.py, tests/test_hook_category_injection.py, docs/user/CONFIG.md, docs/user/COMMANDS.md, CHANGELOG/v4.md
Adds default-off category matching to UserPromptSubmit, deterministic reranking, bounded missed-member additions, <category-focus> output, fail-soft handling, and configuration documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: v1 keyword-triggered belief categories with soft injection.
Description check ✅ Passed The description covers the summary, design, linked issue, scope, and tests, so it is mostly complete despite not mirroring every template heading.
Linked Issues check ✅ Passed The implementation matches #1126’s main requirements: additive tables, category CRUD, CLI/slash surfaces, default-off advisory hook behavior, and documentation/tests.
Out of Scope Changes check ✅ Passed The changes are all tied to the new belief-category feature; no unrelated code paths or surprise side work stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-1126-belief-categories

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 1936 changed lines (limit: 200)
  • 15 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label Jul 15, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/aelfrice/hook.py Outdated
Comment thread src/aelfrice/category.py Outdated
Comment thread src/aelfrice/cli.py Outdated
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
@github-actions

Copy link
Copy Markdown

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 ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@robotrocketscience robotrocketscience removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
…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.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Design changed after R&D — now rerank-on-trigger, not a separate block

Ran empirical R&D on the original design (separate <belief-category-rules> block). It refuted the approach, so the mechanism changed. Full write-up in docs/design/belief_categories.md § R&D that shaped the design; summary:

  • R1 — double-injection (confirmed). A locked rule in a fired keyword category injected twice — once in the category block, once in the L0 <aelfrice-memory> block. All seed categories default locked, so every fired seed category duplicated content L0 already injects.
  • R2 — the block was redundant (confirmed). Across fresh/aged beliefs, tiny/45k-belief stores, and token budgets 200→2400, a category member was always already present in the retrieval output — no realistic case surfaced net-new content. (Two methodology traps found and corrected along the way: tiny synthetic stores over-retrieve, and a freshly-inserted belief ranks #0 for any query, including "weather in paris".)
  • R3 — keyword recall (quantified). Literal-phrase matching missed ~39% of natural phrasings. Expanding the git-workflow seed keywords cut it to ~17% (the residual is the deterministic-matching floor).

New mechanism: hook._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 one-line <category-focus> label. One injection, no duplicate block; deterministic; fail-soft (hits pass through unchanged on disable/no-fire/error).

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 577da52 and 15f9224.

📒 Files selected for processing (15)
  • CHANGELOG/v4.md
  • docs/design/belief_categories.md
  • docs/user/COMMANDS.md
  • docs/user/CONFIG.md
  • docs/user/SLASH_COMMANDS.md
  • src/aelfrice/category.py
  • src/aelfrice/cli.py
  • src/aelfrice/hook.py
  • src/aelfrice/slash_commands/category.md
  • src/aelfrice/store.py
  • tests/test_belief_categories_store.py
  • tests/test_category_module.py
  • tests/test_cli_category.py
  • tests/test_hook_category_injection.py
  • tests/test_slash_commands.py

Comment thread src/aelfrice/cli.py
Comment thread src/aelfrice/hook.py
Comment thread src/aelfrice/store.py
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
@github-actions

Copy link
Copy Markdown

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 ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
…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.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 15, 2026
@github-actions
github-actions Bot merged commit 5e7e52f into main Jul 15, 2026
29 checks passed
@github-actions

Copy link
Copy Markdown

merge-train: merged 5e7e52fmain via FF push.

@robotrocketscience
robotrocketscience deleted the feat/issue-1126-belief-categories branch July 15, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:review Needs review (PR open, awaiting reviewer) author-Setr PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: keyword-triggered belief categories (v1: soft injection)

1 participant