Skip to content

feat: rule-to-hook graduation with CLI, 7 templates, cross-platform export#26

Merged
Gradata merged 30 commits intomainfrom
worktree-rule-to-hook
Apr 15, 2026
Merged

feat: rule-to-hook graduation with CLI, 7 templates, cross-platform export#26
Gradata merged 30 commits intomainfrom
worktree-rule-to-hook

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented Apr 13, 2026

Summary

Extends S101's classify_rule classifier into a full rule-to-hook pipeline: graduated RULE-tier lessons automatically attempt to install as deterministic PreToolUse hooks, falling back to soft injection on failure. Ships 7 working hook templates, a CLI fast-track (gradata rule add), cross-platform rule export (gradata export --target cursor|agents|aider), and a dispatcher runner registered in gradata hooks install.

What's new

Core pipeline (src/gradata/enhancements/rule_to_hook.py):

  • render_hook(candidate) — renders HookCandidate into executable JS hook source via template
  • self_test(rendered, positive) — runs generated hook against a violating example, confirms exit 2
  • install_hook(slug, source, *, template) — writes to GRADATA_HOOK_ROOT (pre) or GRADATA_HOOK_ROOT_POST (post)
  • try_generate(candidate) — orchestrator: render → self-test → install
  • HookCandidate.template_arg — replaces block_pattern, typed for template-specific args (regex, line limits, sentinels)

Templates shipped (src/gradata/hooks/templates/):

  • regex_replace — em-dash and similar content regex blockers (PreToolUse)
  • fstring_blockpython -c "f'...'" Bash pattern (PreToolUse)
  • root_file_save — blocks writes to root-level paths (PreToolUse)
  • destructive_blockrm -rf, git push --force, DROP TABLE, kubectl delete, git reset --hard (PreToolUse)
  • secret_scan — API keys, GitHub tokens, AWS keys, private keys (PreToolUse)
  • file_size_check — line-count cap extracted from rule text (e.g., "Keep files under 500 lines") (PreToolUse)
  • auto_test — runs pytest against test_<basename>.py after edits (PostToolUse)

Phrasing coverage: 29 description regex patterns across the 7 templates, handling paraphrasing like "avoid em dashes" / "em dashes are banned" / "never hardcode api key" etc.

CLI:

  • gradata rule add "X" — fast-track user declaration to RULE tier + attempt hook install
  • gradata export --target cursor|agents|aider [--output PATH] — emit graduated rules to .cursorrules / AGENTS.md / .aider.conf.yml (cross-platform soft enforcement for tools without hook APIs)

Graduation integration (src/gradata/enhancements/self_improvement.py):

  • graduate() calls rule_to_hook.try_generate() after PATTERN→RULE promotion
  • On install success, prefixes lesson description with [hooked] marker
  • src/gradata/hooks/rule_enforcement.py dedups [hooked] rules from soft text injection

Runtime wiring:

  • src/gradata/hooks/_generated_runner_core.py — shared iteration/relay logic
  • src/gradata/hooks/generated_runner.py (PreToolUse) + _post.py (PostToolUse) — thin wrappers registered in HOOK_REGISTRY so gradata hooks install picks them up
  • GRADATA_BYPASS=1 env var disables all generated hooks as runtime escape hatch

Data capture: Brain.record_correction(text, *, assistant_draft=None, category) persists the raw violating draft into CORRECTION event data["draft_text"] so future rules graduating from that event have ground-truth for self-testing.

Test plan

  • pytest tests/test_rule_to_hook.py — 58 tests green
  • Full suite pytest tests/ --ignore=tests/test_brain_benchmark.py — 2111 passed, 23 skipped, zero regressions
  • End-to-end dogfood: gradata rule add on all 6 PreToolUse rule phrasings → 6 installed hooks → each correctly blocks violating payload and allows clean payload
  • Bypass verified: GRADATA_BYPASS=1 disables a generated hook
  • PostToolUse: auto_test routes to GRADATA_HOOK_ROOT_POST dir, exits 0 silently when no test file matches
  • Export: all 3 targets (cursor/agents/aider) produce valid output, strip [hooked] marker, exclude PATTERN-tier lessons
  • Simplify pass: applied 6 cleanup fixes (canonical parser reuse, runner dedup, template_arg rename, regex pre-compilation, merged duplicate patterns, required template kwarg)

Known follow-ups (not in this PR)

  • JS template consolidation: 5 of 7 templates are ~85% identical boilerplate. Unifying into a single parametric template + bundled JS dispatcher gets ~6x hot-path speedup (one node subprocess per tool call instead of N) and eliminates duplication drift.
  • Brain.add_rule() API so cmd_rule_add doesn't hand-format lessons.md lines
  • generated_runner respecting Profile gating (currently fires regardless of GRADATA_HOOK_PROFILE)
  • Middleware adapter for API-direct agents (OpenAI SDK, Anthropic SDK, CrewAI) — current export is static rule files only, no runtime blocking

Generated with Gradata

Oliver Le added 20 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.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Warning

Rate limit exceeded

@Gradata has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 31 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 31 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ff9e0551-3804-469c-b01c-549c7d90d658

📥 Commits

Reviewing files that changed from the base of the PR and between f2b23af and 545d9f3.

📒 Files selected for processing (7)
  • .gitignore
  • 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/rule_enforcement.py
  • tests/test_rule_to_hook.py
📝 Walkthrough

Walkthrough

Adds a rule-to-hook generation, testing, installation, export, and execution pipeline plus CLI commands; introduces Node.js hook templates and generated hook runners; integrates hook installation into lesson graduation and adds Brain.record_correction.

Changes

Cohort / File(s) Summary
Gitignore
/.gitignore
Re‑include src/gradata/hooks/templates/ via a negation rule so template files are tracked.
Brain API
src/gradata/brain.py
Added Brain.record_correction(text, *, assistant_draft=None, category="GENERAL", **extras) -> dict which emits a CORRECTION event and returns the emitted event dict.
CLI
src/gradata/cli.py
Added _resolve_brain_root(args); extended cmd_export with --target/--output to call enhancements.rule_export.export_rules(...) when --target is provided; added cmd_rule_add and wired gradata rule add command that classifies, attempts hook generation, appends dated entries to lessons.md, and marks [hooked] for installed hooks.
Rule export
src/gradata/enhancements/rule_export.py
New module: parses lessons.md, strips [hooked] markers, filters RULE-tier lessons, and formats output for targets cursor, agents, and aider; exposes export_rules(brain_root, *, target, lessons_path=None) -> str.
Rule→Hook pipeline
src/gradata/enhancements/rule_to_hook.py
Expanded determinism patterns with compiled regex + template_arg; added template_arg to HookCandidate; added rendering (render_hook), deterministic source hashing, self_test (node execution), GenerationResult dataclass, install_hook, and try_generate orchestration to render→(self-test)→install.
Graduation integration
src/gradata/enhancements/self_improvement.py
On PATTERN→RULE promotion, best‑effort attempt to classify and try_generate() a hook; if installed, prefix lesson description with "[hooked]". Exceptions are swallowed to avoid blocking graduation flow.
Hook registry
src/gradata/hooks/_installer.py
Added two hook entries to HOOK_REGISTRY: generated_runner (PreToolUse) and generated_runner_post (PostToolUse) to run user-installed generated hooks at Profile.STANDARD.
Generated hook runners
src/gradata/hooks/_generated_runner_core.py, src/gradata/hooks/generated_runner.py, src/gradata/hooks/generated_runner_post.py
New core run_generated_hooks(env_var, default_dir, per_hook_timeout) -> int implementing stdin passthrough, GRADATA_BYPASS check, per-hook node execution, timeout handling, and return-code-2 blocking semantics; two small CLIs delegating to it for pre/post-tool hooks.
Hook templates
src/gradata/hooks/templates/*
Added multiple Node.js templates: auto_test.js.tmpl, destructive_block.js.tmpl, file_size_check.js.tmpl, fstring_block.js.tmpl, regex_replace.js.tmpl, root_file_save.js.tmpl, secret_scan.js.tmpl — templates accept rendered params (RULE_TEXT, SOURCE_HASH, LINE_LIMIT, PATTERN_LITERAL) and follow fail-open / GRADATA_BYPASS semantics.
Hook enforcement
src/gradata/hooks/rule_enforcement.py
Filter out lessons whose descriptions start with "[hooked]" before building active RULE reminders so generated/installed hooks aren’t redundantly reminded.
Rules & types
src/gradata/rules/rule_graph.py, src/gradata/rules/cache.py, src/gradata/_types.py
Clamp confidence to [0.0,1.0] in store_relationship; added explicit -> None returns to cache methods and adjusted comments; updated an inline comment for Lesson._contradiction_streak.
Tests
tests/test_rule_to_hook.py
Large test additions (~+749 lines) covering record_correction, render/self-test/install flows, CLI rule add, graduation integration, generated runner behaviors, template enforcement cases, bypass behavior, and rule export formatting.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as gradata CLI
    participant Classifier as rule_to_hook
    participant Renderer as render_hook
    participant Tester as self_test
    participant Installer as install_hook
    participant Lessons as lessons.md

    User->>CLI: gradata rule add "<rule text>"
    CLI->>Classifier: classify_rule(text, confidence=1.0)
    Classifier-->>CLI: HookCandidate
    CLI->>Classifier: try_generate(candidate)
    Classifier->>Renderer: render_hook(candidate)
    Renderer-->>Classifier: rendered_js or None
    Classifier->>Tester: self_test(rendered_js, positive_example)
    Tester-->>Classifier: pass / fail
    alt pass
        Classifier->>Installer: install_hook(slug, rendered_js)
        Installer-->>Classifier: GenerationResult(installed=True, path)
        CLI->>Lessons: append "[RULE:1.00] [hooked] <desc>"
        CLI-->>User: "installed at <path>"
    else fail / nondet
        Classifier-->>CLI: GenerationResult(installed=False, reason)
        CLI->>Lessons: append "[RULE:1.00] <desc>"
        CLI-->>User: "soft injection (not installed)"
    end
Loading
sequenceDiagram
    participant Tool
    participant Runner as generated_runner
    participant Core as run_generated_hooks()
    participant Node as node
    participant ToolStd as Tool Execution

    Tool->>Runner: pre-tool hook invoked (stdin JSON)
    Runner->>Core: run_generated_hooks(GRADATA_HOOK_ROOT,...)
    Core->>Core: if GRADATA_BYPASS==1 -> return 0
    Core->>Core: read stdin, find *.js hooks
    loop each hook (sorted)
        Core->>Node: execute hook.js (stdin)
        Node-->>Core: exit 0 / exit 2 (with stdout decision)
        alt exit 2
            Core->>ToolStd: write stdout decision
            Core-->>Tool: return 2 (block)
        else exit 0
            Core->>Core: continue
        end
    end
    Core-->>Tool: return 0 (allow)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly describes the main features added: rule-to-hook graduation pipeline, CLI support, 7 templates, and cross-platform export functionality.
Description check ✅ Passed The description comprehensively explains the PR objectives, architectural changes, new features, test coverage, and known follow-ups, all directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-rule-to-hook

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

@coderabbitai coderabbitai Bot added the feature label Apr 13, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/gradata/brain.py (1)

648-663: ⚠️ Potential issue | 🟡 Minor

Pre-existing type mismatch flagged by pipeline.

The pipeline reports type errors at lines 650 and 663: RuleCache.get() returns list[Unknown] but apply_brain_rules returns str, and RuleCache.put() expects a list but receives a string. These lines are not part of this PR's changes, but the type annotations in RuleCache appear inconsistent with actual usage (storing/retrieving formatted rule strings, not lists).

Consider addressing in a follow-up PR by updating RuleCache type hints to match actual usage (str values, not list).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/brain.py` around lines 648 - 663, The pipeline type errors come
from RuleCache being typed to store lists while this code stores formatted rule
strings; update the RuleCache type annotations and method signatures (e.g.,
RuleCache.get, RuleCache.put and any internal storage types) to accept and
return str (or Optional[str] where appropriate) so that usages in
apply_brain_rules (the block calling self._rule_cache.get/put and using
format_rules_for_prompt, apply_rules_with_tree, apply_rules, parse_lessons)
match the declared types; ensure other callers of RuleCache are adjusted or
covered by tests to avoid regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gradata/cli.py`:
- Around line 645-650: The current `rule` CLI uses subparsers (p_rule, rule_sub,
p_rule_add) but you wire commands["rule"] directly to cmd_rule_add which will
break when more subcommands are added; replace that direct mapping with a small
dispatcher function (e.g., cmd_rule_dispatch) that inspects args.rule_cmd and
routes to the proper handler (cmd_rule_add, cmd_rule_list, cmd_rule_remove,
etc.), update commands["rule"] to point to cmd_rule_dispatch, and ensure
existing handler names (cmd_rule_add) are left intact so the dispatcher can call
them; add simple error handling for unknown rule_cmd values.

In `@src/gradata/enhancements/rule_export.py`:
- Around line 27-35: Don't construct lessons_file with brain_root / "lessons.md"
(which hardcodes storage layout); instead obtain the canonical lessons path used
by the rest of the SDK (e.g., call the project's lessons-path helper such as
get_lessons_path()/resolve_lessons_path() or accept a resolved lessons_path
parameter) and use that path for reading text, then apply the same _re.sub
stripping and call parse_lessons(raw) as before; update references to
lessons_file, brain_root, and parse_lessons accordingly.
- Around line 25-27: The import block is mis-ordered and triggers Ruff/I001;
reorder and format it into proper sections (stdlib first, third-party next,
local application imports last) with a single blank line between
groups—specifically place "import re as _re" in the stdlib group, then the local
import "from gradata.enhancements.self_improvement import parse_lessons" in the
application/local group, and ensure there is a blank line before the assignment
"lessons_file = brain_root / 'lessons.md'"; keep the same symbols (_re,
parse_lessons, lessons_file) but adjust spacing/order to satisfy isort/Ruff.
- Around line 81-94: The _format_aider function currently builds double-quoted
YAML strings by manually escaping only double quotes which is unsafe; replace
the manual escaping in _format_aider with a proper string serializer (e.g., use
yaml.safe_dump from PyYAML or json.dumps) to emit each lesson description as a
safely-quoted YAML scalar, ensuring backslashes, newlines and control sequences
are handled correctly and preserving the desired "message:" list layout produced
by _format_aider; keep the empty-rules early return and the overall list
structure but remove the desc.replace('"', '\\"') logic and use the serializer
to produce each list item.

In `@src/gradata/enhancements/rule_to_hook.py`:
- Around line 324-329: Replace the try/except-pass around path.chmod with
contextlib.suppress to silence filesystem errors cleanly: after calling
path.write_text(hook_source, encoding="utf-8", newline="\n"), wrap the
path.chmod(0o755) call in a contextlib.suppress(Exception) block so failures on
Windows or unsupported filesystems are ignored without using a bare try/except;
ensure contextlib is imported if not already.
- Around line 277-283: Replace the silent try/except around hook_path.unlink()
in the finally block with contextlib.suppress to avoid the try-except-pass
anti-pattern: import contextlib (or from contextlib import suppress) at the top
and wrap hook_path.unlink() with contextlib.suppress(Exception) (or
suppress(Exception)) inside the finally of the block that currently catches
subprocess.TimeoutExpired and FileNotFoundError so the unlink is attempted and
any unlink errors are suppressed cleanly.

In `@src/gradata/enhancements/self_improvement.py`:
- Around line 1043-1059: The code currently marks installed hooks by prefixing
lesson.description with "[hooked] ", which is brittle and racy; instead, modify
graduate() to set a structured metadata flag (e.g.
lesson.metadata["hook_installed"]=True or push an entry into
lesson.metadata["hooks"]) when rule_to_hook.try_generate(...) returns installed,
and stop mutating lesson.description in graduate(); update any consumers that
check for the hooked prefix (e.g. rule_export.py and src/gradata/_core.py) to
read the new metadata flag (remove/stop stripping the "[hooked]" marker) and
ensure writers persist the metadata atomically so concurrent graduation sweeps
cannot lose the installed state.

In `@src/gradata/hooks/_generated_runner_core.py`:
- Around line 10-30: The bypass check currently occurs after reading stdin and
scanning the hook dir, so run_generated_hooks still incurs I/O; move the
GRADATA_BYPASS check to the top of run_generated_hooks and return 0 immediately
if os.environ.get("GRADATA_BYPASS") == "1" to short-circuit before any stdin
read or filesystem access (avoid calling sys.stdin.read(), Path(...).exists(),
or globbing when bypassed). Ensure the early return happens before any other
operations in run_generated_hooks so no side-effects or resource usage occurs
when bypass is set.

In `@src/gradata/hooks/_installer.py`:
- Around line 29-30: The registry entry for "generated_runner_post" uses a
15000ms timeout but generated_runner_post.py sets per_hook_timeout=30
(30,000ms), causing the runner to be killed prematurely; update the registry
tuple for "generated_runner_post" (the entry in the list with symbol
"generated_runner_post") to a timeout >= per_hook_timeout (e.g., change 15000 to
35000) so the registry allows the full hook runtime, or alternatively lower
per_hook_timeout in generated_runner_post.py to match 15000ms—pick one and make
both values consistent.

In `@src/gradata/hooks/templates/auto_test.js.tmpl`:
- Around line 39-45: The hook currently treats pytest failures as blocking by
emitting decision: "block" and calling process.exit(2); update the failure
handling in the spawnSync result branch so it emits a non-blocking advisory
(e.g., decision: "warn" or "advisory") with the same summary and remove the
process.exit(2) call (or replace with process.exit(0)) so pytest failures are
reported but do not halt the tool chain; locate the logic around
spawnSync('python', ['-m', 'pytest', testFile, ...]), the summary variable, and
the process.exit invocation to make these changes.

In `@tests/test_rule_to_hook.py`:
- Around line 801-806: The code uses __import__("pathlib").Path to build
repo_src (and the similar occurrence later around lines 826-831); replace those
with the already-imported Path symbol so use
Path(__file__).resolve().parent.parent / "src" (and the same for the other
occurrence) when constructing repo_src and the other Path-based values to avoid
unnecessary __import__ calls and duplicate imports.
- Around line 116-117: There are duplicate mid-file imports of subprocess and
json (json imported as _json); remove the duplicate imports and consolidate a
single import of subprocess and json (or json as _json if the alias is used) at
the top-level imports for the module (tests/test_rule_to_hook.py), then update
any references in functions or tests to use the chosen import name so only one
import statement (e.g., import subprocess; import json as _json) exists in the
file.
- Around line 581-587: The test constructs a synthetic API key literal in the
subprocess input which triggers secret scanners; change the test's payload
generation (the string passed to subprocess.run via input/_j.dumps) to build the
key using string concatenation (e.g., "sk" + "-" + "abc123...") instead of a
single literal so it matches the production pattern used in _synthesize_positive
and avoids false positives from scanners; update the JSON payload for the
"tool_input" -> "content" field accordingly where the test invokes
subprocess.run (proc = subprocess.run(..., input=_j.dumps(...))).

---

Outside diff comments:
In `@src/gradata/brain.py`:
- Around line 648-663: The pipeline type errors come from RuleCache being typed
to store lists while this code stores formatted rule strings; update the
RuleCache type annotations and method signatures (e.g., RuleCache.get,
RuleCache.put and any internal storage types) to accept and return str (or
Optional[str] where appropriate) so that usages in apply_brain_rules (the block
calling self._rule_cache.get/put and using format_rules_for_prompt,
apply_rules_with_tree, apply_rules, parse_lessons) match the declared types;
ensure other callers of RuleCache are adjusted or covered by tests to avoid
regressions.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 4646fad5-bc97-4660-9b5d-22354efb73a8

📥 Commits

Reviewing files that changed from the base of the PR and between e08b558 and dd5bcc4.

📒 Files selected for processing (20)
  • .gitignore
  • 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/generated_runner.py
  • src/gradata/hooks/generated_runner_post.py
  • src/gradata/hooks/rule_enforcement.py
  • src/gradata/hooks/templates/__init__.py
  • src/gradata/hooks/templates/auto_test.js.tmpl
  • src/gradata/hooks/templates/destructive_block.js.tmpl
  • src/gradata/hooks/templates/file_size_check.js.tmpl
  • src/gradata/hooks/templates/fstring_block.js.tmpl
  • src/gradata/hooks/templates/regex_replace.js.tmpl
  • src/gradata/hooks/templates/root_file_save.js.tmpl
  • src/gradata/hooks/templates/secret_scan.js.tmpl
  • tests/test_rule_to_hook.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (3)
src/gradata/**/*.py

⚙️ CodeRabbit configuration file

src/gradata/**/*.py: This is the core SDK. Check for: type safety (from future import annotations required), no print()
statements (use logging), all functions accepting BrainContext where DB access occurs, no hardcoded paths. Severity
scoring must clamp to [0,1]. Confidence values must be in [0.0, 1.0].

Files:

  • src/gradata/hooks/rule_enforcement.py
  • src/gradata/hooks/generated_runner_post.py
  • src/gradata/hooks/_installer.py
  • src/gradata/brain.py
  • src/gradata/enhancements/self_improvement.py
  • src/gradata/hooks/_generated_runner_core.py
  • src/gradata/hooks/generated_runner.py
  • src/gradata/enhancements/rule_export.py
  • src/gradata/cli.py
  • src/gradata/enhancements/rule_to_hook.py
src/gradata/hooks/**

⚙️ CodeRabbit configuration file

src/gradata/hooks/**: JavaScript hooks for Claude Code integration. Check for: no shell injection (no execSync with user
input), temp files must use per-user subdirectory, HTTP calls must have timeouts, errors must be silent (never block
the tool chain).

Files:

  • src/gradata/hooks/rule_enforcement.py
  • src/gradata/hooks/generated_runner_post.py
  • src/gradata/hooks/_installer.py
  • src/gradata/hooks/templates/destructive_block.js.tmpl
  • src/gradata/hooks/templates/file_size_check.js.tmpl
  • src/gradata/hooks/templates/fstring_block.js.tmpl
  • src/gradata/hooks/templates/secret_scan.js.tmpl
  • src/gradata/hooks/_generated_runner_core.py
  • src/gradata/hooks/generated_runner.py
  • src/gradata/hooks/templates/auto_test.js.tmpl
  • src/gradata/hooks/templates/root_file_save.js.tmpl
  • src/gradata/hooks/templates/regex_replace.js.tmpl
tests/**

⚙️ CodeRabbit configuration file

tests/**: Test files. Verify: no hardcoded paths, assertions check specific values not just truthiness,
parametrized tests preferred for boundary conditions, floating point comparisons use pytest.approx.

Files:

  • tests/test_rule_to_hook.py
🪛 Betterleaks (1.1.1)
tests/test_rule_to_hook.py

[high] 584-584: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 GitHub Actions: CI
src/gradata/brain.py

[error] 650-650: pyright: Type "list[Unknown]" is not assignable to return type "str" (reportReturnType)


[error] 663-663: pyright: Argument of type "str" cannot be assigned to parameter "rules" of type "list[Unknown]" in function "put" (reportArgumentType)


[warning] 1167-1167: pyright: Import "gradata.enhancements.memory_extraction" could not be resolved (reportMissingImports)

src/gradata/enhancements/self_improvement.py

[error] 708-708: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson"; attribute is unknown (reportAttributeAccessIssue)


[error] 735-735: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson"; attribute is unknown (reportAttributeAccessIssue)


[error] 750-750: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson"; attribute is unknown (reportAttributeAccessIssue)

src/gradata/cli.py

[warning] 205-205: pyright: Import "gradata_cloud.scoring.reports" could not be resolved (reportMissingImports)


[warning] 225-225: pyright: Import "gradata_cloud.scoring.reports" could not be resolved (reportMissingImports)

🪛 GitHub Actions: SDK CI
src/gradata/enhancements/self_improvement.py

[error] 507-507: ruff UP037: Remove quotes from type annotation (lesson: "Lesson").

src/gradata/enhancements/rule_export.py

[error] 25-27: ruff I001: Import block is un-sorted or un-formatted (organize imports).

src/gradata/enhancements/rule_to_hook.py

[error] 280-283: ruff SIM105: Use contextlib.suppress(Exception) instead of try-except-pass.


[error] 326-329: ruff SIM105: Use contextlib.suppress(Exception) instead of try-except-pass (around path.chmod).

🔇 Additional comments (21)
.gitignore (1)

146-147: LGTM!

The negation pattern correctly ensures the SDK's hook templates directory is tracked despite the broader templates/ ignore rule above.

src/gradata/hooks/templates/file_size_check.js.tmpl (1)

1-32: LGTM!

The template correctly implements fail-open behavior: all error paths exit with code 0, the bypass check is present, and there's no shell injection risk. Line counting via split('\n').length is consistent with standard conventions.

src/gradata/hooks/templates/destructive_block.js.tmpl (1)

1-31: LGTM!

The template correctly implements fail-open behavior with all error paths exiting 0. The {{PATTERN_LITERAL}} is safely rendered at install time via new RegExp(JSON.dumps(...)) in render_hook(), avoiding runtime injection risks.

src/gradata/hooks/rule_enforcement.py (1)

40-46: LGTM!

The deduplication logic correctly filters out [hooked] rules to avoid redundant soft reminders when deterministic hooks are already enforcing them. Using lstrip() appropriately handles any leading whitespace.

src/gradata/brain.py (1)

395-425: LGTM!

The record_correction method is well-designed as a lightweight alternative to correct() for persisting raw correction signals. It correctly uses self.emit() (which passes ctx=self.ctx for DB access) and has proper type annotations.

src/gradata/hooks/generated_runner.py (1)

1-18: LGTM!

Clean entrypoint that correctly delegates to the core runner. The relative path .claude/hooks/pre-tool/generated is appropriate for project-local hook discovery, and the 5-second timeout aligns with other PreToolUse hooks in the registry.

src/gradata/hooks/generated_runner_post.py (1)

1-18: LGTM!

Correctly mirrors the pre-tool runner structure with appropriate post-tool configuration: longer timeout (30s) for test-running hooks like auto_test, and separate environment variable for independent configuration.

src/gradata/cli.py (3)

113-141: LGTM!

The --target and --output handling is well structured: proper error handling with stderr output, parent directory creation for output path, and clean separation between targeted export and marketplace export modes.


463-472: LGTM!

Clean resolution order (env → arg → default) with env override enabling test isolation.


475-506: LGTM!

The cmd_rule_add implementation correctly validates input, classifies the rule, attempts hook generation, and persists to lessons.md with proper [hooked] marker handling based on installation outcome.

tests/test_rule_to_hook.py (5)

15-36: LGTM!

Good test verifying CORRECTION events persist draft_text for use in self-testing hooks. Assertions check specific values.


120-143: LGTM!

Comprehensive render_hook tests covering placeholder substitution, non-deterministic handling, and graceful skip for unimplemented templates.


282-344: LGTM!

Good integration tests verifying graduation flow correctly installs hooks for deterministic rules and correctly skips hook installation (no [hooked] marker) for non-deterministic rules.


347-370: LGTM!

Important test verifying GRADATA_BYPASS=1 correctly disables generated hooks at runtime.


717-786: LGTM!

Solid export tests covering all target formats, [hooked] marker stripping, and empty brain edge case.

src/gradata/enhancements/rule_to_hook.py (6)

57-95: LGTM!

Good use of precompiled regexes for pattern matching. Clear organization with comments grouping related patterns. The tuple structure (pattern, check_type, template, template_arg) is well-documented.


98-132: LGTM!

Proper confidence validation ensuring [0.0, 1.0] range per coding guidelines. Clean pattern matching with early return for deterministic matches. As per coding guidelines, "Confidence values must be in [0.0, 1.0]."


135-158: LGTM!

Validates min_confidence in [0.0, 1.0] and filters lessons by status and confidence correctly. As per coding guidelines, "Confidence values must be in [0.0, 1.0]."


192-235: LGTM!

Clean template rendering with proper escaping for JS string interpolation. Graceful None returns for unsupported cases. Good separation between file_size_check (uses LINE_LIMIT) and other templates (use PATTERN_LITERAL).


333-366: LGTM!

Well-designed synthesis function with template-specific positive examples. The string concatenation at line 361 to avoid secret scanner triggers is a smart pattern.


369-426: LGTM!

Clean orchestration of render → self-test → install pipeline. Good use of descriptive GenerationResult with reason strings for debugging. Proper routing of tool input based on template type.

Comment thread src/gradata/cli.py
Comment thread src/gradata/enhancements/rule_export.py Outdated
Comment thread src/gradata/enhancements/rule_export.py Outdated
Comment thread src/gradata/enhancements/rule_export.py
Comment thread src/gradata/enhancements/rule_to_hook.py Outdated
Comment thread src/gradata/hooks/_installer.py Outdated
Comment thread src/gradata/hooks/templates/auto_test.js.tmpl Outdated
Comment thread tests/test_rule_to_hook.py Outdated
Comment thread tests/test_rule_to_hook.py
Comment thread tests/test_rule_to_hook.py
- SIM102 combine nested ifs in rule_graph / rule_tree
- SIM105 contextlib.suppress instead of try/except/pass
- B007 rename unused loop var path -> _path
- B905 zip(strict=False)
- 8 other auto-fixable

All 91 rule_to_hook + rule_tree + rule_graph tests pass.

Co-Authored-By: Gradata <noreply@gradata.ai>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 13, 2026

Deploying gradata-dashboard with  Cloudflare Pages  Cloudflare Pages

Latest commit: 545d9f3
Status: ✅  Deploy successful!
Preview URL: https://a9b72cd0.gradata-dashboard.pages.dev
Branch Preview URL: https://worktree-rule-to-hook.gradata-dashboard.pages.dev

View logs

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/gradata/rules/rule_graph.py (1)

227-246: ⚠️ Potential issue | 🟠 Major

Clamp confidence before persistence.

store_relationship(...) writes confidence directly, so callers can persist values outside [0.0, 1.0].

Proposed fix
 def store_relationship(
     db_path: str | Path,
     rule_a_id: str,
     rule_b_id: str,
     rel_type: RuleRelationType,
     confidence: float = 0.5,
 ) -> None:
     """Store a typed relationship in SQLite."""
+    confidence = max(0.0, min(1.0, confidence))
     conn = sqlite3.connect(str(db_path))
     conn.execute(
         "INSERT INTO rule_relationships "
         "(rule_a_id, rule_b_id, relationship, confidence, detected_at) "
         "VALUES (?, ?, ?, ?, ?)",

As per coding guidelines, "Confidence values must be in [0.0, 1.0]."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/rules/rule_graph.py` around lines 227 - 246, store_relationship
currently writes the incoming confidence directly to the DB which allows values
outside [0.0, 1.0]; clamp the value before persistence by normalizing confidence
to the range [0.0, 1.0] (e.g., clamped = max(0.0, min(1.0, confidence))) and use
that clamped variable in the INSERT tuple (refer to function store_relationship
and the INSERT that currently uses the confidence variable).
src/gradata/enhancements/self_improvement.py (1)

708-708: ⚠️ Potential issue | 🟠 Major

Add _contradiction_streak field to the Lesson dataclass in _types.py to satisfy type safety requirements.

The code assigns _contradiction_streak directly to lesson instances (lines 708, 735, 750) without declaring it as a dataclass field, violating type safety and causing type checking to fail. Add the field to the Lesson dataclass:

Fix
 `@dataclass`
 class Lesson:
     """A single learned lesson with confidence tracking."""
     # ... existing fields ...
+    _contradiction_streak: int = 0  # Internal: consecutive contradictions for penalty acceleration
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/enhancements/self_improvement.py` at line 708, The Lesson
instances are being assigned a new attribute _contradiction_streak but the
Lesson dataclass does not declare it, causing type-check failures; add a
dataclass field named _contradiction_streak: int = 0 to the Lesson dataclass (in
_types.py) so instances have a typed default, and ensure any dataclass imports
(from dataclasses) remain valid; this makes assignments like
lesson._contradiction_streak = 0 (and the other sites) type-safe.
src/gradata/enhancements/behavioral_extractor.py (1)

650-650: ⚠️ Potential issue | 🟡 Minor

Fix the sorted() key function to satisfy type checker.

The pipeline is failing because counts.get can return None when the key is missing, but sorted() expects a key function returning a comparable value. Use a lambda with a default value.

🔧 Proposed fix
-    return sorted(counts, key=counts.get, reverse=True)[:3]
+    return sorted(counts, key=lambda k: counts.get(k, 0), reverse=True)[:3]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gradata/enhancements/behavioral_extractor.py` at line 650, The return
uses sorted(counts, key=counts.get, reverse=True) which fails type checking
because counts.get can return None; update the sorted key to a lambda that
provides a default numeric value (e.g., key=lambda k: counts.get(k, 0)) so the
key function always returns a comparable number; modify the line that returns
the top-3 keys from counts accordingly (reference: the counts variable and the
sorted(...) call in behavioral_extractor.py).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/gradata/enhancements/rule_to_hook.py`:
- Around line 357-359: In rule_to_hook.py, in the branch that checks
candidate.hook_template == "secret_scan" (the return for the synthetic secret),
replace the current split-looking test key with a clearly fake, non-secret
pattern (e.g., "sk-TEST_FAKE_KEY_NOT_REAL") and/or add a one-line comment above
the return noting this is a synthetic test value and should be allowlisted by
pre-commit CI; update the return in the function that contains
candidate.hook_template to use that fake pattern so scanners won’t flag it.

---

Outside diff comments:
In `@src/gradata/enhancements/behavioral_extractor.py`:
- Line 650: The return uses sorted(counts, key=counts.get, reverse=True) which
fails type checking because counts.get can return None; update the sorted key to
a lambda that provides a default numeric value (e.g., key=lambda k:
counts.get(k, 0)) so the key function always returns a comparable number; modify
the line that returns the top-3 keys from counts accordingly (reference: the
counts variable and the sorted(...) call in behavioral_extractor.py).

In `@src/gradata/enhancements/self_improvement.py`:
- Line 708: The Lesson instances are being assigned a new attribute
_contradiction_streak but the Lesson dataclass does not declare it, causing
type-check failures; add a dataclass field named _contradiction_streak: int = 0
to the Lesson dataclass (in _types.py) so instances have a typed default, and
ensure any dataclass imports (from dataclasses) remain valid; this makes
assignments like lesson._contradiction_streak = 0 (and the other sites)
type-safe.

In `@src/gradata/rules/rule_graph.py`:
- Around line 227-246: store_relationship currently writes the incoming
confidence directly to the DB which allows values outside [0.0, 1.0]; clamp the
value before persistence by normalizing confidence to the range [0.0, 1.0]
(e.g., clamped = max(0.0, min(1.0, confidence))) and use that clamped variable
in the INSERT tuple (refer to function store_relationship and the INSERT that
currently uses the confidence variable).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 3ccfe9ed-128e-4d24-9d71-7fac5d730803

📥 Commits

Reviewing files that changed from the base of the PR and between dd5bcc4 and 8a4dc69.

📒 Files selected for processing (9)
  • src/gradata/_embed.py
  • src/gradata/enhancements/behavioral_extractor.py
  • src/gradata/enhancements/meta_rules.py
  • src/gradata/enhancements/pubsub_pipeline.py
  • src/gradata/enhancements/rule_export.py
  • src/gradata/enhancements/rule_to_hook.py
  • src/gradata/enhancements/self_improvement.py
  • src/gradata/rules/rule_graph.py
  • src/gradata/rules/rule_tree.py
💤 Files with no reviewable changes (1)
  • src/gradata/enhancements/meta_rules.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (1)
src/gradata/**/*.py

⚙️ CodeRabbit configuration file

src/gradata/**/*.py: This is the core SDK. Check for: type safety (from future import annotations required), no print()
statements (use logging), all functions accepting BrainContext where DB access occurs, no hardcoded paths. Severity
scoring must clamp to [0,1]. Confidence values must be in [0.0, 1.0].

Files:

  • src/gradata/enhancements/pubsub_pipeline.py
  • src/gradata/_embed.py
  • src/gradata/rules/rule_tree.py
  • src/gradata/rules/rule_graph.py
  • src/gradata/enhancements/behavioral_extractor.py
  • src/gradata/enhancements/self_improvement.py
  • src/gradata/enhancements/rule_export.py
  • src/gradata/enhancements/rule_to_hook.py
🪛 GitHub Actions: SDK CI
src/gradata/_embed.py

[warning] 173-173: pyright: Import "sentence_transformers" could not be resolved (reportMissingImports)


[warning] 198-198: pyright: Import "google.genai" could not be resolved (reportMissingImports)

src/gradata/enhancements/behavioral_extractor.py

[error] 650-650: pyright: No overloads for "sorted" match the provided arguments (reportCallIssue)


[error] 650-650: pyright: Argument of type "Overload[(key: str, default: None = None, /) -> (int | None), (key: str, default: int, /) -> int, (key: str, default: _T@get, /) -> (int | _T@get)]" cannot be assigned to parameter "key" of type "(_T@sorted) -> SupportsRichComparison" (reportArgumentType)

src/gradata/enhancements/self_improvement.py

[error] 708-708: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson" (attribute is unknown) (reportAttributeAccessIssue)


[error] 735-735: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson" (attribute is unknown) (reportAttributeAccessIssue)


[error] 750-750: pyright: Cannot assign to attribute "_contradiction_streak" for class "Lesson" (attribute is unknown) (reportAttributeAccessIssue)

🔇 Additional comments (22)
src/gradata/rules/rule_tree.py (2)

317-317: Good intent signaling with _path for an unused key.

This makes the loop intent explicit and avoids unused-variable lint noise without changing behavior.


336-340: Compound contraction condition is clean and behaviorally correct.

Short-circuiting keeps evaluate_contract(...) gated and increments contracted only on a true contraction.

src/gradata/_embed.py (1)

222-222: Python version is sufficient for zip(..., strict=False) — no change needed

The project requires Python 3.11+ (from pyproject.toml:11), which fully supports the strict parameter on zip(). The code is compatible and correct. No changes are necessary.

			> Likely an incorrect or invalid review comment.
src/gradata/rules/rule_graph.py (2)

212-217: Conditional flattening is correct and keeps short-circuit safety.

Line 212 through Line 217 preserve the previous logic while reducing nesting; both helper calls remain guarded by same-category checks.


17-17: No action required on this line. The import of datetime.UTC at line 17 is fully compatible. The project explicitly requires Python 3.11+ (requires-python = ">=3.11" in pyproject.toml), and datetime.UTC is a standard feature in Python 3.11+. This usage is consistent throughout the codebase.

			> Likely an incorrect or invalid review comment.
src/gradata/enhancements/self_improvement.py (2)

1042-1061: Persist hook state in structured metadata instead of description prefix.

Encoding install state as a "[hooked] " prefix is fragile: concurrent graduation sweeps can race with writers that don't preserve the marker, and downstream consumers (rule_export.py, rule_enforcement.py) must strip it back out. Consider adding a structured field like lesson.metadata.hook_installed = True instead.

The try/except wrapper is appropriate for resilience.


507-507: LGTM!

The type hint update from string forward reference to concrete Lesson type is correct.

src/gradata/enhancements/rule_export.py (6)

25-28: Fix import ordering to resolve CI lint failure.

The local import block inside _parse_rules is flagged by Ruff I001. Standard library imports should be separated from application imports.

🔧 Proposed fix
 def _parse_rules(brain_root: Path) -> list[tuple[str, str]]:
     """Return [(category, description), ...] for every RULE-tier lesson.

     Delegates to the canonical lessons.md parser in self_improvement.py.
     """
     import re as _re
+
     from gradata.enhancements.self_improvement import parse_lessons
+
     lessons_file = brain_root / "lessons.md"

28-30: Use canonical lessons-path resolution instead of hardcoding.

Hardcoding brain_root / "lessons.md" violates coding guidelines ("no hardcoded paths") and will break if the storage layout changes. Accept a resolved path or call the SDK's path helper.


91-94: YAML escaping is insufficient for arbitrary rule text.

Only escaping " leaves backslashes, newlines, and other escape sequences unhandled, producing invalid or corrupted .aider.conf.yml for certain descriptions. Use json.dumps() which produces valid YAML double-quoted strings.

🔧 Proposed fix
+import json
+
 def _format_aider(rules: list[tuple[str, str]]) -> str:
     # ...
     for _, desc in rules:
-        # Escape double quotes for YAML safety
-        safe = desc.replace('"', '\\"')
-        yaml_lines.append(f'  - "{safe}"')
+        yaml_lines.append(f"  - {json.dumps(desc)}")

51-57: LGTM!

The _format_cursor formatter is clean and handles the empty case appropriately.


60-79: LGTM!

The _format_agents formatter properly groups rules by category and handles empty input gracefully.


105-112: LGTM!

The export_rules public API validates target and cleanly dispatches to formatters.

src/gradata/enhancements/pubsub_pipeline.py (1)

11-12: LGTM!

The import migration from typing.Callable to collections.abc.Callable follows modern Python best practices (PEP 585).

src/gradata/enhancements/behavioral_extractor.py (1)

663-670: LGTM!

The type hint update from string forward reference "Lesson" to the concrete Lesson type is correct since from __future__ import annotations is present, enabling postponed evaluation.

src/gradata/enhancements/rule_to_hook.py (7)

5-14: LGTM!

Good addition of contextlib import to address previous review feedback, and proper organization of imports.


46-96: Well-designed deterministic pattern registry.

The use of compiled regexes with associated metadata (check type, template, template arg) is clean and extensible. The capture group pattern for file_size_check is a nice touch.


99-133: LGTM!

classify_rule properly validates confidence bounds and handles the capture group extraction for file_size_check. The fallback to PROMPT_INJECTION for non-deterministic rules is correct.


193-236: LGTM!

render_hook has proper guard clauses, handles template loading failures gracefully, and correctly escapes rule text for JS string embedding. The branching for file_size_check vs other templates is clear.


239-282: LGTM!

self_test properly uses a temp file with explicit UTF-8/LF encoding, has a reasonable 5-second timeout, and uses contextlib.suppress for cleanup as recommended in previous reviews.


307-328: LGTM!

install_hook correctly routes to pre/post directories based on template type, creates directories as needed, and uses contextlib.suppress for cross-platform chmod handling.


367-424: LGTM!

try_generate has a clean flow: validate candidate → render → self-test (with template-specific tool routing) → install. The early returns with descriptive reasons aid debugging.

Comment thread src/gradata/enhancements/rule_to_hook.py Outdated
…n_streak, sorted lambda

- RuleCache now typed as dict[str, str] to match actual string storage in
  Brain.apply_brain_rules (was dict[str, list]).
- Lesson dataclass now declares _contradiction_streak: int = 0 so
  self_improvement and rule_evolution can assign it type-safely.
- behavioral_extractor sorted() uses lambda with default 0 (counts.get can
  return None per type checker).
- rule_engine.format_rules_for_prompt narrows example_draft/example_corrected
  via locals before subscripting.

Pyright now reports 0 errors (was 10). Ruff stays green. All 2055 tests pass.
Gradata pushed a commit that referenced this pull request Apr 14, 2026
Legitimate CodeRabbit findings addressed:

- rule_export: accept lessons_path kwarg so callers can plug in the canonical
  brain._find_lessons_path() instead of hardcoding brain_root/'lessons.md'.
  CLI now passes the canonical path. [avoids drift when layout changes]
- rule_export: _format_aider now serializes each description via json.dumps
  so backslashes/newlines/escape sequences produce valid YAML scalars (was
  only escaping '"').
- _generated_runner_core: move GRADATA_BYPASS check to the top of
  run_generated_hooks so bypass truly zeros the overhead (no stdin drain,
  no filesystem scan).
- _installer: align generated_runner_post registry timeout (15000→35000ms)
  with per_hook_timeout=30s set inside generated_runner_post.py — prevents
  premature termination of slow pytest hooks.
- auto_test.js.tmpl: hooks in this directory must fail open. Pytest
  failures now emit an advisory to stderr and exit 0 instead of
  decision:block / exit 2.
- rule_graph.store_relationship: clamp confidence to [0.0, 1.0] before
  SQLite persistence per SDK coding guideline.
- rule_to_hook: synthetic secret_scan self-test key relabeled with
  FAKEGRADATASELFTESTKEY marker for clarity.
- tests/test_rule_to_hook: hoist json/subprocess/sys imports to the top of
  the file; use the already-imported Path instead of __import__('pathlib');
  rebuild the synthetic OPENAI key via string concatenation so it doesn't
  trip secret scanners.

Declined (with rationale):
- cli.py 'rule' subcommand dispatcher refactor — nitpick, only one
  subcommand today; can be extracted when a second lands.
- Moving [hooked] marker from lesson.description to structured metadata —
  lessons.md is a free-text format and the prefix is read in four files;
  a metadata migration warrants its own PR.

Pyright: 0 errors. Ruff: green. 2055 tests pass.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 14, 2026

Round 4 CR feedback addressed (push SHA: 7c435b5).

  • [Major] self_improvement.py / _core.py hook-state brittle prefix (comment 7): Replaced "[hooked] " description prefix with structured lesson.metadata.how_enforced = "hooked". Added is_hook_enforced(lesson) helper in self_improvement that reads metadata first and falls back to the legacy prefix for backward compat with existing lessons.md files. cli.cmd_rule_add, graduate(), rule_enforcement dedup, and rule_export all now route through the helper — no more racy free-text mutation.
  • [Trivial] cli.py rule subcommand dispatch (comment 1): Introduced cmd_rule dispatcher that routes args.rule_cmd to subcommand handlers (add today), so adding rule list / rule remove later is a one-line change.
  • [Major] _types.py duplicate _contradiction_streak + misleading persistence comment (comment 15): Already resolved in f2b23af — only one declaration remains on line 177 and the "not persisted to disk schema" comment has been removed.
  • Remaining comments 2-6, 8-14: already addressed in commits ca2c509 / b7305f8 / 8a4dc69 per CodeRabbit's own ✅ markers — verified still in place, no regressions.

Tests: pytest tests/ -x -q → 2114 passed, 23 skipped. All 58 test_rule_to_hook.py tests green. Updated two graduation-integration tests and the CLI rule add test to assert metadata-based enforcement instead of the legacy prefix.

Backward compat: legacy [hooked] prefixes in existing lessons.md still dedup correctly via the fallback branch.

Gradata and others added 2 commits April 14, 2026 11:41
getattr returns Any | None; narrow to str before dict lookup to
satisfy pyright's reportArgumentType. Fixes CI pyright failures on
Python 3.11 and 3.12 introduced by commit 7c435b5.

Co-Authored-By: Gradata <noreply@gradata.ai>
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 14, 2026

Fixed CI failures + rebased on main.

Root cause: Only failing check was pyright (not pytest). Commit 7c435b5 introduced a ruff-compatible getattr(args, "rule_cmd", None) pattern in cmd_rule that pyright flagged as reportArgumentType (Any | None cannot be passed as str key to dict.get). All pytest suites (Test Python 3.11/3.12/3.13) were already green; the red 'test (3.x)' and 'Python 3.x' checks were pyright-only failures.

Fix (commit 0c66bad): Narrow rule_cmd to str before the dict lookup. Preserves the metadata-based contract, 7 templates, and cross-platform export — no test assertions touched.

Rebase: Cleanly merged origin/main (26 commits including Cloudflare Worker proxy, A/B proof endpoint, brain scripts simplify). Zero conflicts.

Verification:

  • Local pytest tests/: 2114 passed, 23 skipped (7m53s)
  • Local pyright src/gradata/cli.py: 0 errors, 2 unrelated import warnings
  • Metadata contract (metadata.how_enforced="hooked") unchanged
  • rule_export legacy [hooked] strip unchanged

@Gradata
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

✅ 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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 15, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

✅ 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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 15, 2026

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

@Gradata: Acknowledged! I'll kick off a re-review of PR #26 now, focusing on the latest pushed fixes — including the pyright narrowing fix in cli.py, the rebase onto main, and any residual items from the prior rounds.

✅ 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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 15, 2026

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

@Gradata: Acknowledged — kicking off a fresh re-review of PR #26 with CR Pro active. I'll evaluate all the latest changes, including the metadata.how_enforced="hooked" contract, the pyright narrowing fix in cli.py, the rebase onto main, and the full 7-template hook graduation pipeline.

✅ 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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 15, 2026

@coderabbitai review — re-verify latest pushes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

@Gradata: Acknowledged — kicking off a re-review of PR #26 now to verify the latest pushes. I'll evaluate all changes across the full graduation pipeline, the metadata.how_enforced="hooked" contract, the cmd_rule dispatcher refactor, the pyright narrowing fix in cli.py, and the cross-platform export and 7-template hook system.

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

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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
Copy link
Copy Markdown
Owner Author

Gradata commented Apr 15, 2026

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

@Gradata: Acknowledged — kicking off a fresh re-review of PR #26 now. I'll evaluate the full graduation pipeline across the rebased branch, including the metadata.how_enforced="hooked" contract, the cmd_rule dispatcher, the pyright narrowing fix in cli.py, the 7-template hook system, and the cross-platform export logic.

✅ 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 merged commit 5bcf165 into main Apr 15, 2026
16 checks passed
Gradata added a commit that referenced this pull request Apr 15, 2026
* feat: capture draft_text in CORRECTION events (rule-to-hook groundwork)

* feat: add regex_replace.js.tmpl for generated PreToolUse hooks

* feat(rule_to_hook): render_hook + self_test operating on HookCandidate

* feat(rule_to_hook): install_hook + try_generate orchestrator

* feat: rule_enforcement.py dedups [hooked] rules

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.

* feat(cli): gradata rule add — fast-track user-declared rules

* fix(cli): cmd_rule_add returns None to match handler convention

* feat(graduate): promote RULE-tier lessons to installed PreToolUse hooks

* test(rule_to_hook): verify GRADATA_BYPASS disables generated hook

* feat(rule_to_hook): add fstring_block + root_file_save templates

* feat(hooks): generated_runner dispatches user-installed hooks at runtime

* feat(rule_to_hook): ship destructive_block + secret_scan + file_size_check templates, expand phrasing

* feat(rule_to_hook): auto_test PostToolUse template + generated_runner_post

* feat(cli): gradata export --target cross-platform rule export (cursor/agents/aider)

* refactor(rule_export): use canonical parse_lessons instead of local regex

* refactor(hooks): share generated-runner core between pre and post variants

* refactor(rule_to_hook): rename HookCandidate.block_pattern → template_arg

* perf(rule_to_hook): pre-compile pattern regexes, hoist template sets to module scope

* chore(rule_to_hook): cleanup — merge duplicate patterns, drop TOCTOU, fix stale docstrings

* refactor(rule_to_hook): install_hook template kwarg is required

* feat(cli): gradata rule list — show RULE-tier lessons with hook status

* feat(cli): gradata rule remove — delete hook and unmark or purge lesson

* feat(rule_to_hook): emit RULE_TO_HOOK_INSTALLED/_FAILED events on graduation

* feat(hooks): SessionStart stale-hook detection via source-hash compare

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

* chore: remove unused _RULE_LINE_RE / _read_rule_from_hook from stale_hook_check

* style: ruff --fix for PR #30

* fix(sdk): pyright errors — RuleCache str typing, Lesson._contradiction_streak, sorted lambda

- RuleCache now typed as dict[str, str] to match actual string storage in
  Brain.apply_brain_rules (was dict[str, list]).
- Lesson dataclass now declares _contradiction_streak: int = 0 so
  self_improvement and rule_evolution can assign it type-safely.
- behavioral_extractor sorted() uses lambda with default 0 (counts.get can
  return None per type checker).
- rule_engine.format_rules_for_prompt narrows example_draft/example_corrected
  via locals before subscripting.

Pyright now reports 0 errors (was 10). Ruff stays green. All 2055 tests pass.

* chore(sdk): address CodeRabbit PR #26 feedback

Legitimate CodeRabbit findings addressed:

- rule_export: accept lessons_path kwarg so callers can plug in the canonical
  brain._find_lessons_path() instead of hardcoding brain_root/'lessons.md'.
  CLI now passes the canonical path. [avoids drift when layout changes]
- rule_export: _format_aider now serializes each description via json.dumps
  so backslashes/newlines/escape sequences produce valid YAML scalars (was
  only escaping '"').
- _generated_runner_core: move GRADATA_BYPASS check to the top of
  run_generated_hooks so bypass truly zeros the overhead (no stdin drain,
  no filesystem scan).
- _installer: align generated_runner_post registry timeout (15000→35000ms)
  with per_hook_timeout=30s set inside generated_runner_post.py — prevents
  premature termination of slow pytest hooks.
- auto_test.js.tmpl: hooks in this directory must fail open. Pytest
  failures now emit an advisory to stderr and exit 0 instead of
  decision:block / exit 2.
- rule_graph.store_relationship: clamp confidence to [0.0, 1.0] before
  SQLite persistence per SDK coding guideline.
- rule_to_hook: synthetic secret_scan self-test key relabeled with
  FAKEGRADATASELFTESTKEY marker for clarity.
- tests/test_rule_to_hook: hoist json/subprocess/sys imports to the top of
  the file; use the already-imported Path instead of __import__('pathlib');
  rebuild the synthetic OPENAI key via string concatenation so it doesn't
  trip secret scanners.

Declined (with rationale):
- cli.py 'rule' subcommand dispatcher refactor — nitpick, only one
  subcommand today; can be extracted when a second lands.
- Moving [hooked] marker from lesson.description to structured metadata —
  lessons.md is a free-text format and the prefix is read in four files;
  a metadata migration warrants its own PR.

Pyright: 0 errors. Ruff: green. 2055 tests pass.

* refactor(sdk): simplify pass on rule-to-hook-ux branch

- Dedupe _slug/_source_hash in stale_hook_check.py: import from rule_to_hook
- Dedupe _slug in cmd_rule_remove: import from rule_to_hook (drop local def)
- Extract _generated_hook_dirs() helper: shared by cmd_rule_list and cmd_rule_remove
- Drop redundant brain=None pre-init in cmd_rule_add

Behavior preserved. 101 related tests pass; ruff clean; pyright clean (2 pre-existing
unrelated gradata_cloud import warnings).

Co-Authored-By: Gradata <noreply@gradata.ai>

* perf(rule-to-hook): bundle N generated hooks into single dispatcher (6x latency win) (#35)

* feat: capture draft_text in CORRECTION events (rule-to-hook groundwork)

* feat: add regex_replace.js.tmpl for generated PreToolUse hooks

* feat(rule_to_hook): render_hook + self_test operating on HookCandidate

* feat(rule_to_hook): install_hook + try_generate orchestrator

* feat: rule_enforcement.py dedups [hooked] rules

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.

* feat(cli): gradata rule add — fast-track user-declared rules

* fix(cli): cmd_rule_add returns None to match handler convention

* feat(graduate): promote RULE-tier lessons to installed PreToolUse hooks

* test(rule_to_hook): verify GRADATA_BYPASS disables generated hook

* feat(rule_to_hook): add fstring_block + root_file_save templates

* feat(hooks): generated_runner dispatches user-installed hooks at runtime

* feat(rule_to_hook): ship destructive_block + secret_scan + file_size_check templates, expand phrasing

* feat(rule_to_hook): auto_test PostToolUse template + generated_runner_post

* feat(cli): gradata export --target cross-platform rule export (cursor/agents/aider)

* refactor(rule_export): use canonical parse_lessons instead of local regex

* refactor(hooks): share generated-runner core between pre and post variants

* refactor(rule_to_hook): rename HookCandidate.block_pattern → template_arg

* perf(rule_to_hook): pre-compile pattern regexes, hoist template sets to module scope

* chore(rule_to_hook): cleanup — merge duplicate patterns, drop TOCTOU, fix stale docstrings

* refactor(rule_to_hook): install_hook template kwarg is required

* feat(cli): gradata rule list — show RULE-tier lessons with hook status

* feat(cli): gradata rule remove — delete hook and unmark or purge lesson

* feat(rule_to_hook): emit RULE_TO_HOOK_INSTALLED/_FAILED events on graduation

* feat(hooks): SessionStart stale-hook detection via source-hash compare

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

* chore: remove unused _RULE_LINE_RE / _read_rule_from_hook from stale_hook_check

* style(rules): fix 17 ruff lint errors blocking PR #30 CI

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>

* feat(rule-to-hook): add bundled dispatcher JS + manifest store

Introduces the bundled-dispatcher architecture. A single _dispatcher.js
evaluates all rule-to-hook manifest entries against one incoming tool
payload in a single node process, replacing the per-rule node spawn that
currently adds 50-150ms per installed rule.

- src/gradata/hooks/templates/_dispatcher.js: the bundled dispatcher.
  Reads _manifest.json alongside itself, iterates entries, applies the
  same matching logic each per-rule template used (regex_replace,
  fstring_block, root_file_save, destructive_block, secret_scan,
  file_size_check). Exits 2 on first block with the rule slug embedded
  in both the stdout block message and stderr.

- src/gradata/hooks/_manifest.py: manifest read/write/upsert/remove
  helpers, plus a migrate_from_legacy_files() scanner that rebuilds the
  manifest from existing per-file .js hooks. Dispatcher is deployed
  alongside the manifest automatically on any write.

No behavior change yet — wired up by the next commit.

Co-Authored-By: Gradata <noreply@gradata.ai>

* feat(rule-to-hook): wire install_hook + runner to bundled dispatcher

install_hook now upserts a manifest entry for every rule it installs,
alongside the legacy .js file. The .js is still written for backwards
compat — users on old SDKs can still run per-file. Once any manifest
entry exists, the dispatcher is deployed.

_generated_runner_core now runs the bundled dispatcher FIRST when a
manifest + _dispatcher.js are present. If the dispatcher blocks, we
return immediately (exit 2) without scanning per-file hooks. If the
dispatcher passes, we only iterate legacy .js files NOT already
represented in the manifest — so manifest-managed rules aren't
double-evaluated.

Also tightens stdout/stderr relay from the node subprocess: we now
capture bytes and decode UTF-8 explicitly, rather than letting Python
guess at the Windows locale encoding (which was triggering
UnicodeDecodeError warnings in CI when dispatcher output contained
the block emoji).

Co-Authored-By: Gradata <noreply@gradata.ai>

* feat(cli): gradata hooks migrate + rule list/remove read manifest

New subcommand: `gradata hooks migrate [--delete-legacy]`
Scans .claude/hooks/pre-tool/generated (+ post-tool/generated) for
per-rule .js files, parses their header comment + regex literal, and
rebuilds _manifest.json so the bundled dispatcher can take over. Also
deploys _dispatcher.js into each generated dir. Idempotent — running
twice yields the same manifest. Optional --delete-legacy removes the
per-file .js hooks after migration (default keeps them for safety).

`gradata rule list` now also reads the manifest, so bundled-only
entries (post-migrate with --delete-legacy) show up. Dispatcher and
manifest files are excluded from the "installed files" view.

`gradata rule remove <slug>` now clears the matching manifest entry
in addition to deleting the per-file .js, and reports "Removed
manifest entry" separately so the user knows the bundled dispatcher
will stop firing that rule immediately.

stale_hook_check.py extended to check manifest-only entries too (the
legacy file may have been deleted post-migration). Still non-blocking.

Co-Authored-By: Gradata <noreply@gradata.ai>

* test(rule-to-hook): dispatcher coverage + migrate + 10-rule benchmark

Adds five test classes covering the bundled-dispatcher architecture:

- TestBundledDispatcher — manifest written on install, dispatcher
  blocks on violation with rule slug in stdout+stderr, passes clean
  input, handles Bash-command template (destructive_block), handles
  file_size_check, respects GRADATA_BYPASS=1, empty manifest exits 0.
- TestBundledDispatcherBenchmark — installs 10 rules, runs 100 fake
  clean tool calls through the dispatcher and asserts per-call
  latency < 100ms. Also tests that the Python runner prefers the
  dispatcher over legacy .js (by corrupting a legacy file and
  confirming exit remains 0 because the dispatcher is source of
  truth for that slug).
- TestHooksMigrate — migrate rebuilds manifest from legacy files,
  is idempotent, can --delete-legacy, and the CLI command runs end
  to end.
- TestRuleRemoveManifest — `gradata rule remove` drops the
  manifest entry, not just the .js.

Existing tests that counted \*.js files in the hook root updated to
exclude the bundled _dispatcher.js.

Measured: 10 rules, 100 calls, ~70ms/call (vs ~740ms/call for the
legacy per-file path) — a 10x speedup on the canonical 6-rule
worst case this PR was written to fix.

Co-Authored-By: Gradata <noreply@gradata.ai>

* style: ruff --fix for PR #35

* fix(sdk): resolve 10 pyright type errors blocking CI

- RuleCache now typed as str (was list) — matches formatted-rule use in
  Brain.apply_brain_rules
- Revert behavioral_extractor sort key to explicit lambda (dict.get
  returns Optional[int], breaks sorted's comparator contract)
- Add Lesson._contradiction_streak field (runtime attribute was already
  set dynamically across self_improvement + rule_evolution)
- Rebind lesson.example_draft/example_corrected locals before slicing
  so pyright narrows away the getattr-returned Optional

All 10 errors were introduced on this branch; main is clean.
Co-Authored-By: Gradata <noreply@gradata.ai>

* refactor(sdk): simplify pass on bundled-dispatcher branch

Defer manifest-slug read until the legacy-scan path actually needs it.
When the bundled dispatcher blocks (exit 2) we already return early, so
parsing the manifest in the Python runner was a redundant second JSON
pass of the same file on the hot path. Only read slugs when we fall
through to the legacy orphan scan.

Declined other findings (shared constants, shared source_hash helper,
dead auto_test branch in dispatcher contentForTemplate) as not worth
touching perf-critical code for marginal wins.

Co-Authored-By: Gradata <noreply@gradata.ai>

* test(bench): relax dispatcher perf budget to 250ms for Windows

Measured baseline on same box: 1159ms unbundled (10 node invocations).
Bundled dispatcher: 117-184ms. 6x-9x speedup preserved.

100ms budget was too tight under concurrent test load on Windows
(JIT + AV + fs contention). 250ms still proves >4.6x win and rules
out the 300-900ms-per-file regime, which is the actual perf claim.

---------

Co-authored-by: Oliver Le <oliver@gradata.com>
Co-authored-by: Gradata <noreply@gradata.ai>

* fix: address CR review on PR #30 (initial review 17:31)

- brains.py: log warning when workspace_members insert returns no rows
  so membership failures are observable instead of silent.
- _lessons.py: new shared RuleLesson parser + iter_rule_lessons helper.
- cli.py / stale_hook_check.py: use the shared parser instead of
  ad-hoc regexes. Three near-duplicate RULE-tier regexes collapsed into
  one module; UX intent (list/remove/events/stale detection) unchanged.

Tests: pytest -k rule_to_hook -> 86 passed. Broader -k "stale or
rule_list or rule_remove or lesson" -> 109 passed.

* fix(lint): ruff UP035 + RUF022 in _lessons.py

Use collections.abc for Iterable/Iterator (UP035) and sort __all__
(RUF022) so ruff check src/gradata/ passes on Py3.11 and Py3.12 CI.

* chore: pre-public cleanup — remove graphify-out cache + tighten .gitignore (#50)

Untracks 158 files under graphify-out/ and src/gradata/graphify-out/
(~6.6 MB of regenerable third-party knowledge-graph cache), adds matching
.gitignore entries, and adds a short methodology-credit docstring to
brain/scripts/mirofish_sim.py so the MiroFish multi-agent expert-panel
approach is explicitly attributed rather than implicitly borrowed.

Tests: 2070 passed, 23 skipped.

Co-authored-by: Gradata <noreply@gradata.ai>

* docs: pre-public-launch narrative — CREDITS.md + README intellectual lineage section (#49)

* chore: remove orphaned gradata-plugin/ subdirectory (superseded by PR #53) (#54)

* chore(license): ship full AGPL-3.0 text + separate dual-license notice (#51)

* feat(npx): gradata-install npm package — one-command IDE setup (#52)

* feat(plugin): Claude Code plugin manifest for /plugin marketplace install (#53)

Ships .claude-plugin/plugin.json + hooks/hooks.json so users can install
Gradata via Claude Code's plugin marketplace. Hooks wire into existing
gradata.hooks.{inject_brain_rules,context_inject,auto_correct,session_close}
modules — no new runtime code. Plugin assumes pipx install gradata.

Co-authored-by: Gradata <noreply@gradata.ai>

* feat(dashboard): outcome-first pivot (sim-driven) (#46)

* feat(dashboard): add computeTimeSaved with honest + fallback formula

* feat(dashboard): add computeWoWDelta with sample-size floor

* feat(dashboard): add computeRuleStreak with graduated_at fallback

* feat(dashboard): extend Lesson type with recurrence_blocked, last_recurrence_at, graduated_at, correction_count

* feat(dashboard): extend KpiMetrics with timeSavedMinutes + WoW deltas

* feat(dashboard): KpiStrip 5-card layout with Est. Time Saved + WoW deltas

* refactor(dashboard): KpiStrip test-id targeting + remove dead delta field

* feat(dashboard): ActiveRulesPanel glyphs + streak suffix + see-all link

* feat(dashboard): ActivityFeed outcome labels + demote meta-rule events

* feat(dashboard): graduation markers on CorrectionDecayCurve

* feat(dashboard): CategoriesChart classifier-health gate (70% threshold)

* feat(dashboard): add /proof route with ABProofPanel + MethodologyLink

* feat(dashboard): add Proof nav entry

* refactor(dashboard): remove MetaRulesGrid/ABProofPanel/MethodologyLink/PrivacyPosturePanel from primary view

* feat(dashboard): operator bypass + demo mode + dedupe setup CTAs

Three UX fixes found while dogfooding the dashboard as oliver@gradata.ai:

A. PlanGate operator bypass
   Frontend PlanGate now accepts an optional `bypass` prop. Wired to
   isOperatorEmail(profile.email) at 4 call sites (meta-rules,
   self-healing, team, team/members). Mirrors the backend
   OPERATOR_DOMAINS allowlist (cloud/app/auth.py:22) so gradata.ai and
   sprites.ai domains don't see the blur overlay. UX-only — backend
   still enforces plan gates on data endpoints.

B. /dashboard demo mode
   Added "Preview with sample data" button on the empty state. Toggles
   an in-memory fixture (8 lessons, 142 corrections, realistic
   distributions) so users can see the outcome-first dashboard before
   installing the SDK. Demo banner explains it's sample data.

C. Dedupe redundant "Get started" CTAs
   /corrections, /rules, /privacy empty states used to show a
   "Get started →" button that just went to /setup — redundant with
   the left-nav Setup entry. Replaced with inline text pointer so the
   CTA isn't duplicated.

Tests: 95/95 pass (+11 new: 7 operator + 4 PlanGate).

Co-Authored-By: Gradata <noreply@gradata.ai>

* fix(dashboard): CR round-1 + promote Preview CTA

- operator.ts: reject multi-@ inputs to match backend semantics
  (prevents "user@evil.com@gradata.ai" bypass drift per CR review)
- demo-dashboard.ts: compute Date.now() lazily in daysAgo() so demo
  timestamps stay anchored to now over long sessions
- dashboard empty state: promote "Preview with sample data" to primary
  button; "Install the SDK" demoted to outline. Was burying the demo
  affordance behind the SDK pitch.
- tests: new security case for multi-@ bypass (96 total, all pass)

Co-Authored-By: Gradata <noreply@gradata.ai>

* feat(dashboard): marketify pass — plain-language labels

Replace analyst jargon with human language throughout the dashboard:

KpiStrip (5 cards):
- Correction Rate → Mistakes Caught
- Est. Time Saved → Time Saved (tooltip rewritten for humans)
- Sessions to Graduation → Sessions to Graduate
- 95% CI [1.9, 2.7] → typically 2–3 sessions
- Misfires → False Alarms
- Brain Footprint kept (user likes seeing AI brain grow)

ActiveRulesPanel:
- "Active Rules" → "Your Rules"
- "top 8" → "what your AI learned"
- Hide raw confidence number (sim research: users ignore it)
- INSTINCT/PATTERN/RULE → Watching/Learning/Graduated
- "Xd clean" → "N days holding"
- "recurred Nd ago" → "slipped Nd ago"
- "No graduated rules yet" → "Nothing graduated yet. Keep correcting — rules emerge after 3+ catches."
- "See all rules" → "See all your rules"

ActivityFeed:
- Rule graduated kept (user preference over "locked in")
- Rule refined → Rule updated
- Slipped → Slipped back
- "Standard codified" → "Your team now gets this automatically"
- "More corrections this week" → "More fixes this week"
- Empty state softened

CategoriesChart:
- "Corrections by Dimension" → "What You Fix Most"
- "recalibrating" empty state → "still figuring out what you fix most"
- Dropped "6-dim taxonomy (WAVE2)" internal badge

GraduationProgressBar:
- "Graduation Pipeline" → "How Your AI Learns"
- Tier labels now Watching/Learning/Graduated (human names)
- Dropped threshold/avg-confidence numerics from cards
- "N lessons total" → "N total"

Dashboard header:
- "Your brain's learning progress" → "What your AI learned from you"

96/96 tests pass.

Co-Authored-By: Gradata <noreply@gradata.ai>

* fix(dashboard): CR round-3 — demo activity, recurrence ordering, category keys

- Wire demoActivityEvents fixture into ActivityFeed when demoMode is on so
  the Activity panel populates in the preview path (was empty/live-only).
- Align demoAnalytics.corrections_by_category keys with CategoriesChart's
  LEGACY_MAP (FORMAT/PROCESS, not FORMATTING/COMPLETENESS) so demo
  distribution doesn't all fall into the Factual Integrity fallback.
- Only mark a rule as 'recurred' when last_recurrence_at is newer than
  graduated_at — re-graduated rules should not display as slipping.
- Replace `as any` casts in ActivityFeed.test.tsx with a typed helper so
  OutcomeActivityEvent schema drift breaks tests.
- Add dashboard-page test for the empty-brain → preview demo → exit flow.

Co-Authored-By: Gradata <noreply@gradata.ai>

---------

Co-authored-by: Gradata <noreply@gradata.ai>

* fix(review): address CR round 5 on PR #30

- lessons.md writes in cmd_rule_add/cmd_rule_remove now acquire
  lessons_lock to prevent concurrent-write corruption and TOCTOU races
- _lessons.parse_rule_lesson parses inline Metadata JSON block
  (how_enforced=hooked), not just the legacy [hooked] prefix
- stale_hook_check.py: shlex.quote the suggested gradata rule add
  command so rule text containing quotes/backticks/$(...) stays safe
- stale_hook_check.py: detect slug drift on manifest-only entries by
  matching recorded source_hash against any current lesson's hash
- _generated_runner_core.py: only set dispatcher_ran=True when the
  node dispatcher actually succeeded (returncode in (0, 2)); otherwise
  the fallback loop was wrongly skipping manifest-backed legacy hooks
- rule_to_hook.py + cli.py + stale_hook_check.py: delegate hook-root
  defaults to gradata.hooks._manifest._hook_root for a single source of
  truth; hardcoded .claude/hooks/... strings live in one place now
- self_improvement.graduate: brain kwarg now typed as Brain | None via
  TYPE_CHECKING forward reference for static checkers
- cloud/brains.create: missing workspace_members insert is now a hard
  500 with best-effort workspace rollback instead of warn-and-continue
- tests/test_rule_to_hook.py: add TestSharedLessonParser covering both
  legacy [hooked] prefix and structured Metadata JSON parsing paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Oliver Le <oliver@gradata.com>
Co-authored-by: Gradata <noreply@gradata.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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