Skip to content

feat(sdk): Brain.add_rule API + profile gating in runners + codex/cline/continue exports#31

Merged
Gradata merged 42 commits into
mainfrom
feat/rule-api-profile-exports
Apr 15, 2026
Merged

feat(sdk): Brain.add_rule API + profile gating in runners + codex/cline/continue exports#31
Gradata merged 42 commits into
mainfrom
feat/rule-api-profile-exports

Conversation

@Gradata

@Gradata Gradata commented Apr 13, 2026

Copy link
Copy Markdown
Owner

Summary

Three architecture gaps in the SDK rule system, fixed sequentially in one PR:

  1. Brain.add_rule(...) APIcmd_rule_add used to hand-format [date] [STATE:conf] CATEGORY: desc lines directly into lessons.md. When the lesson schema evolves, that hand-formatted writer breaks silently. The new API routes through parse_lessons / format_lessons — the same canonical code path graduation uses — so schema changes auto-propagate. Includes duplicate detection, confidence clamping, state validation, optional data dict with field protection, and LESSON_ADDED event emission.

  2. Profile gating in generated_runner — every other hook in _installer.HOOK_REGISTRY routes through run_hook which calls should_run(profile). The generated_runner + generated_runner_post modules bypass run_hook (they sys.exit(main()) directly), so users on GRADATA_HOOK_PROFILE=minimal were still getting every user-declared rule enforced on every Edit/Write/Bash. Now the runner checks should_run(Profile.STANDARD) up front and no-ops under minimal.

  3. Cross-platform export targetsgradata rule export --target now supports codex, cline, continue on top of the existing cursor/agents/aider. All route through the same parse_lessons pipeline, filter to RULE-tier only, strip internal [hooked] markers. New DEFAULT_PATHS dict exposes the conventional output path per target.

Test plan

  • 17 new tests in tests/test_brain_add_rule.py — happy path, rejection, duplicates, clamp, data dict, event emission
  • 5 new tests in tests/test_hook_profile.py — minimal skip, standard/strict execute, default-to-standard
  • 21 new tests in tests/test_rule_export.py — registry sanity, path uniqueness, empty-brain placeholder, category grouping, [hooked] stripping, RULE-tier filter, regression for existing targets
  • Full suite passes: 2171 passed, 23 skipped in 112s
  • Existing test_rule_to_hook.py::TestCliRuleAdd still passes (cmd_rule_add respects GRADATA_BRAIN env var)

Commits

  • b985188 — feat(sdk): add Brain.add_rule API for programmatic rule creation
  • 0f229e6 — fix(hooks): gate generated_runner on GRADATA_HOOK_PROFILE
  • 5c28402 — feat(sdk): add codex/cline/continue export targets + DEFAULT_PATHS

Oliver Le and others added 29 commits April 12, 2026 17:18
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.
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.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

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).

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- 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

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed round-2 CR (post-retarget) in 70959f0. All 14 actionable items:

brain.py

  • add_rule: category canonicalized to .strip().upper() before dedupe/persist/emit (was "drafting" vs "DRAFTING" dup risk)
  • add_rule: wrapped read/parse/dedupe-check/append/write in lessons_lock to close TOCTOU window
  • record_correction: extras applied first so explicit detail/category/draft_text always win over caller-supplied keys

cli.py

  • cmd_rule_add: captures Brain.add_rule(...) return, prints reason and exits non-zero on failure
  • cmd_rule_list / cmd_rule_remove: regex now accepts optional legacy [hooked] token between state bracket and category (preserves existing group layout)
  • cmd_rule_remove: dropped the duplicated local _slug, reuses rule_to_hook._slug (single source of truth)

rule_export.py

  • _format_aider: uses json.dumps(desc, ensure_ascii=False) for YAML-1.2-compatible double-quoted scalars (handles backslashes, newlines, unicode, not just ")

rule_to_hook.py

  • Tightened never commit secret | no secret | never push secret catch-all into \bno secrets?\b ... \b(in|to|into)\b ... \b(code|commit|commits|repo|source)\b to avoid matching generic phrases

stale_hook_check.py

  • _parse_lessons now routes through canonical parse_lessons(); legacy [RULE] [hooked] CATEGORY: desc and categories with / are recognized. Legacy marker position is preserved via a separate line-level scan.
  • Printed fix command uses shlex.quote(slug) + shlex.quote(current_text) so copy-pasted commands are safe against quotes / metacharacters

auto_test.js.tmpl

  • Handles res.error / res.status === null up front — emits {decision: "allow"} + exit 0 so infra failures don't block the tool chain. Nonzero-status failure path unchanged.

tests

  • test_brain_add_rule: fixture uses monkeypatch.setenv("BRAIN_DIR", ...) — no more os.environ leak
  • test_rule_to_hook: module-level pytestmark = pytest.mark.skipif(shutil.which("node") is None, ...)

Public API preserved: Brain.add_rule signature, profile gating, and cursor/agents/aider/codex/cline/continue exporters unchanged. Full suite: 2171 passed, 23 skipped.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — fixes pushed since last review, please re-verify

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Understood — triggering a fresh incremental review to verify the fixes pushed since the last round. I'll re-examine all the changes, including the commit 70959f0 fixes across brain.py, cli.py, rule_export.py, rule_to_hook.py, stale_hook_check.py, auto_test.js.tmpl, and the test suite.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — CR Pro active, please re-verify.

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Understood — triggering a fresh incremental review now with CR Pro active. I'll re-verify all the latest fixes across brain.py, cli.py, rule_export.py, rule_to_hook.py, stale_hook_check.py, auto_test.js.tmpl, _generated_runner_core.py, and the test suite updates.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — rebased on latest main, all prior fixes applied

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Understood — triggering a fresh incremental review now. I'll re-verify all the latest changes across brain.py, cli.py, rule_export.py, rule_to_hook.py, stale_hook_check.py, _generated_runner_core.py, auto_test.js.tmpl, and the test suite after the rebase on latest main.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…-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
@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

Rebased on latest main after batch merges

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant