feat(sdk): Brain.add_rule API + profile gating in runners + codex/cline/continue exports#31
Conversation
When rule_to_hook graduates a deterministic rule into a generated PreToolUse hook, the soft text reminder becomes noise. Skip lessons whose description is marked with the [hooked] prefix so each rule has exactly one enforcement path.
…check templates, expand phrasing
… fix stale docstrings
Generated hooks carry a Source hash: <12chars> line derived from the rule text at install time. If the user edits the lesson text in lessons.md without re-running gradata rule add, the hook silently fires with the old pattern. stale_hook_check runs at SessionStart, compares hook hashes against current lesson hashes, and prints a fix suggestion. - New module: src/gradata/hooks/stale_hook_check.py (never blocks, exit 0) - HOOK_REGISTRY: register at SessionStart, STANDARD profile - Tests: 4 new cases in TestStaleHookCheck - Handles slug drift: if rule text edit changed the slug, pairs orphan hooks with orphan [hooked] lessons in file order
Auto-fixable (9) via ruff --fix: - UP017 datetime.timezone.utc -> datetime.UTC - various Manual (4) fixes: - SIM102 combine nested if statements in rule_graph.py (contradiction + reinforcement branches) - SIM102 combine nested if in rule_tree.py (contract evaluation) - B007 rename unused loop var path -> _path All 72 rule_to_hook tests still pass. Co-Authored-By: Gradata <noreply@gradata.ai>
cmd_rule_add hand-formatted [date] [STATE:conf] CATEGORY: desc lines directly into lessons.md. When the lesson schema evolves (e.g. adding new metadata fields like alpha/beta_param), that hand-formatted writer breaks silently — graduated rules pick up the new schema but user-declared rules don't. Brain.add_rule(description, category, state='RULE', confidence=0.90, data=None) routes through parse_lessons / format_lessons — the same canonical code path the graduation pipeline uses. Schema changes auto-propagate. Features: - Duplicate detection (category + whitespace-normalized description) - Confidence clamping to [0.0, 1.0] - Unknown state rejection with clear reason - Optional data dict for extra Lesson fields (root_cause, agent_type, etc) — protected fields (date/state/conf/category/description) can't be overridden via data - Emits LESSON_ADDED event for audit trail - Graceful handling of missing lessons.md (creates it) cmd_rule_add now uses Brain.add_rule when the resolved brain has a system.db, else falls back to parse/format helpers (still no hand-formatting). Uses _resolve_brain_root (not _get_brain) to avoid writing to CWD when GRADATA_BRAIN points elsewhere. 17 new tests covering happy path, rejection, duplicates, confidence clamp, data dict handling, event emission.
Every other hook in _installer.HOOK_REGISTRY routes through run_hook() which calls should_run(profile) before executing. generated_runner + generated_runner_post bypass run_hook (they use sys.exit(main()) not run_hook(main, meta)), so they were executing generated rule-hooks even under GRADATA_HOOK_PROFILE=minimal. This breaks the minimal profile contract: users who set minimal expect the core learning loop only, but they were still getting every user-declared rule enforced on every Edit/Write/Bash. Fix: check should_run(Profile.STANDARD) at the top of run_generated_hooks — matches the registry entry. Logs a debug line and exits 0 when skipped. 5 new tests cover minimal skip, standard/strict execute, and unset-profile-defaults-to-standard behavior.
gradata rule export previously supported cursor/agents/aider only — three common AI coding tools (Codex CLI, Cline, Continue.dev) were left out, so users running those had to hand-roll rule files. New targets + default output paths: - codex -> .codex/AGENTS.md (Codex CLI, AGENTS.md convention) - cline -> .clinerules (Cline single-file rules) - continue -> .continue/rules/gradata-rules.md (Continue.dev rules dir) All three use the same parse_lessons pipeline, filter RULE-tier only, strip the internal [hooked] marker, and group rules by category for readability. Output shape is plain markdown (headings + bullets) — idiomatic for each tool's rules format. Also exports a new DEFAULT_PATHS dict so callers can look up the conventional output path for a target without hard-coding strings. CLI --target now accepts the three new choices. 21 new tests cover registry sanity, path uniqueness, empty-brain placeholder, category grouping, [hooked] marker stripping, and RULE-tier filter. Existing cursor/agents/aider regression tests included.
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Retargeted to main — branch was stacked on PR #30 (still open). This PR contains its own standalone work (Brain.add_rule API + profile gating + codex/cline/continue exporters). |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- brain.add_rule: canonicalize category casing (upper) pre-dedupe/persist - brain.add_rule: wrap read/check/append/write in lessons_lock to kill TOCTOU - brain.record_correction: apply extras first so explicit fields win - cli.cmd_rule_add: check add_rule return value and exit non-zero on failure - cli.cmd_rule_list/remove: regex accepts legacy "[hooked]" token position - cli.cmd_rule_remove: reuse canonical _slug from rule_to_hook (dedup) - rule_export._format_aider: json.dumps for YAML-safe description escaping - rule_to_hook: tighten overly-broad secret regex with preposition+noun anchors - stale_hook_check: route through parse_lessons, shlex.quote printed fix cmd - auto_test.js.tmpl: handle spawnSync res.error / res.status===null gracefully - test_brain_add_rule: use monkeypatch.setenv for BRAIN_DIR (scoped) - test_rule_to_hook: pytestmark skipif node missing
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Addressed round-2 CR (post-retarget) in 70959f0. All 14 actionable items: brain.py
cli.py
rule_export.py
rule_to_hook.py
stale_hook_check.py
auto_test.js.tmpl
tests
Public API preserved: |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review — fixes pushed since last review, please re-verify |
|
✅ Actions performedReview triggered.
|
|
@coderabbitai review — CR Pro active, please re-verify. |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
@coderabbitai review — rebased on latest main, all prior fixes applied |
|
✅ Actions performedReview triggered.
|
…-exports # Conflicts: # src/gradata/_types.py # src/gradata/brain.py # src/gradata/cli.py # src/gradata/enhancements/rule_export.py # src/gradata/enhancements/rule_to_hook.py # src/gradata/enhancements/self_improvement.py # src/gradata/hooks/_generated_runner_core.py # src/gradata/hooks/_installer.py # src/gradata/hooks/rule_enforcement.py # src/gradata/hooks/templates/auto_test.js.tmpl # src/gradata/rules/cache.py # tests/test_rule_to_hook.py
|
Rebased on latest main after batch merges |
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Summary
Three architecture gaps in the SDK rule system, fixed sequentially in one PR:
Brain.add_rule(...)API —cmd_rule_addused to hand-format[date] [STATE:conf] CATEGORY: desclines directly intolessons.md. When the lesson schema evolves, that hand-formatted writer breaks silently. The new API routes throughparse_lessons/format_lessons— the same canonical code path graduation uses — so schema changes auto-propagate. Includes duplicate detection, confidence clamping, state validation, optionaldatadict with field protection, andLESSON_ADDEDevent emission.Profile gating in
generated_runner— every other hook in_installer.HOOK_REGISTRYroutes throughrun_hookwhich callsshould_run(profile). Thegenerated_runner+generated_runner_postmodules bypassrun_hook(theysys.exit(main())directly), so users onGRADATA_HOOK_PROFILE=minimalwere still getting every user-declared rule enforced on every Edit/Write/Bash. Now the runner checksshould_run(Profile.STANDARD)up front and no-ops underminimal.Cross-platform export targets —
gradata rule export --targetnow supportscodex,cline,continueon top of the existingcursor/agents/aider. All route through the sameparse_lessonspipeline, filter to RULE-tier only, strip internal[hooked]markers. NewDEFAULT_PATHSdict exposes the conventional output path per target.Test plan
tests/test_brain_add_rule.py— happy path, rejection, duplicates, clamp, data dict, event emissiontests/test_hook_profile.py— minimal skip, standard/strict execute, default-to-standardtests/test_rule_export.py— registry sanity, path uniqueness, empty-brain placeholder, category grouping,[hooked]stripping, RULE-tier filter, regression for existing targetstest_rule_to_hook.py::TestCliRuleAddstill passes (cmd_rule_add respectsGRADATA_BRAINenv var)Commits
b985188— feat(sdk): add Brain.add_rule API for programmatic rule creation0f229e6— fix(hooks): gate generated_runner on GRADATA_HOOK_PROFILE5c28402— feat(sdk): add codex/cline/continue export targets + DEFAULT_PATHS