Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7680808
feat: behavioral extraction + pattern tracking for learning pipeline
Apr 9, 2026
7d9034b
fix: resolve Pyright type error in orchestrator.py
Apr 9, 2026
582abcc
feat: add hook foundation — profiles, base protocol, installer, and C…
Apr 9, 2026
409fa83
feat: add core learning loop hooks — inject_brain_rules, session_clos…
Apr 9, 2026
5c1656c
feat: add safety hooks — secret scan, config protection, rule enforce…
Apr 9, 2026
2edd465
feat(hooks): add 11 intelligence + completeness hooks (STANDARD + STR…
Apr 9, 2026
bf9a57c
refactor(hooks): extract shared helpers, remove duplication, use stdl…
Apr 9, 2026
e9a31c3
refactor(hooks): final cleanup — remove dead code, fix exceptions, co…
Apr 9, 2026
d41eb64
fix(hooks): address all PR #20 review findings — critical, major, min…
Apr 9, 2026
8a95a21
fix(hooks): address all PR #20 review findings — critical through nit…
Apr 9, 2026
29309e2
fix: address all CodeRabbit round 2 findings (31 items)
Apr 9, 2026
237c0f8
fix: address all CodeRabbit round 2 findings
Apr 9, 2026
3422014
fix: address CodeRabbit round 3 P1 findings — SQLite leaks and race c…
Apr 9, 2026
19162ee
merge: resolve conflicts with main (PR #19 meta-rule pipeline)
Apr 9, 2026
30ea6fa
feat(self-healing): add rule failure detector
Apr 9, 2026
201469f
feat(self-healing): wire rule failure detection into brain.correct()
Apr 9, 2026
4fd7ad9
feat(self-healing): add brain.patch_rule() public API
Apr 9, 2026
7c2c7af
feat(self-healing): add retroactive test gate for patch candidates
Apr 9, 2026
f5353a0
feat(self-healing): background review fork + patch generation pipeline
Apr 9, 2026
4c2c27f
feat(self-healing): correction-driven nudging for missing rules
Apr 9, 2026
ef0f176
feat(self-healing): scope narrowing on wrong-context rule failures (P…
Apr 9, 2026
7181dfe
test(self-healing): end-to-end integration test for full self-healing…
Apr 9, 2026
1f38105
refactor(self-healing): simplify after code review
Apr 9, 2026
ec5251c
merge: resolve conflict with main (meta_rules_storage identical lines)
Apr 10, 2026
3c9bf34
fix(lint): resolve all ruff errors + add ruff to dev deps
Apr 10, 2026
5aefb0b
fix(ci): update sdk-ci.yml paths + skip bandit false positives
Apr 10, 2026
5d27f03
fix(test): stabilize graduation test for Python 3.12
Apr 10, 2026
7adea31
fix: address PR #21 CodeRabbit review findings
Apr 10, 2026
c84ac47
merge: resolve conflicts with main after PR #20 squash merge
Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions sdk/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
site_name: Gradata
site_description: AI behavioral adaptation engine. Corrections graduate into rules.
repo_url: https://github.com/gradata-systems/gradata
repo_name: gradata-systems/gradata

theme:
name: material
palette:
- scheme: slate
primary: deep purple
accent: amber
toggle:
icon: material/brightness-4
name: Light mode
- scheme: default
primary: deep purple
accent: amber
toggle:
icon: material/brightness-7
name: Dark mode
features:
- navigation.instant
- navigation.tracking
- navigation.sections
- navigation.expand
- content.code.copy
- content.code.annotate
- search.highlight

nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/install.md
- Quick Start: getting-started/quickstart.md
- Core Concepts: getting-started/concepts.md
- Gradata Cloud:
- Overview: cloud/overview.md
- Getting Started: cloud/quickstart.md
- API Reference: cloud/api.md
- Marketplace: cloud/marketplace.md
- Guides:
- Training a Brain: guides/training.md
- Custom Domains: guides/custom-domains.md
- MCP Integration: guides/mcp.md
- API Reference:
- Brain Class: api/brain.md
- CLI: api/cli.md
- Contributing:
- Architecture: architecture/overview.md
- Patterns: architecture/patterns.md
- Enhancements: architecture/enhancements.md
- Brain Data: architecture/brain.md
- Agent Adaptation: architecture/agent-graduation.md
- Patterns API: api/patterns.md
- Enhancements API: api/enhancements.md
- Agent Adaptation Guide: guides/agent-graduation.md
Comment on lines +30 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if all markdown files referenced in the navigation exist

cd sdk

# List of all files referenced in the navigation
files=(
  "docs/index.md"
  "docs/getting-started/install.md"
  "docs/getting-started/quickstart.md"
  "docs/getting-started/concepts.md"
  "docs/cloud/overview.md"
  "docs/cloud/quickstart.md"
  "docs/cloud/api.md"
  "docs/cloud/marketplace.md"
  "docs/guides/training.md"
  "docs/guides/custom-domains.md"
  "docs/guides/mcp.md"
  "docs/guides/agent-graduation.md"
  "docs/api/brain.md"
  "docs/api/cli.md"
  "docs/api/patterns.md"
  "docs/api/enhancements.md"
  "docs/architecture/overview.md"
  "docs/architecture/patterns.md"
  "docs/architecture/enhancements.md"
  "docs/architecture/brain.md"
  "docs/architecture/agent-graduation.md"
)

missing=()
for file in "${files[@]}"; do
  if [[ ! -f "$file" ]]; then
    missing+=("$file")
  fi
done

if [[ ${`#missing`[@]} -eq 0 ]]; then
  echo "✅ All navigation files exist"
else
  echo "❌ Missing ${`#missing`[@]} file(s):"
  printf '  - %s\n' "${missing[@]}"
  exit 1
fi

Repository: Gradata/gradata

Length of output: 714


🏁 Script executed:

#!/bin/bash
# List the actual structure of the sdk directory
echo "=== sdk directory structure ==="
ls -la sdk/

echo -e "\n=== Check if docs directory exists in sdk ==="
if [ -d "sdk/docs" ]; then
  echo "docs/ directory EXISTS"
  echo "Contents of sdk/docs/:"
  find sdk/docs -type f -name "*.md" 2>/dev/null | head -30
else
  echo "docs/ directory DOES NOT exist"
fi

echo -e "\n=== Search for all .md files in sdk ==="
find sdk -type f -name "*.md" 2>/dev/null | sort

Repository: Gradata/gradata

Length of output: 383


Create missing documentation files referenced in the navigation.

All 21 markdown files referenced in the navigation (lines 30-56) do not exist. The docs/ directory is missing entirely, and the documentation build will fail without these files. Create the required directory structure and documentation files:

  • docs/index.md
  • docs/getting-started/{install,quickstart,concepts}.md
  • docs/cloud/{overview,quickstart,api,marketplace}.md
  • docs/guides/{training,custom-domains,mcp,agent-graduation}.md
  • docs/api/{brain,cli,patterns}.md
  • docs/architecture/{overview,patterns,enhancements,brain,agent-graduation}.md
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/mkdocs.yml` around lines 30 - 56, The mkdocs navigation references many
missing documentation pages so the site build will fail; create the docs
directory and add the exact markdown files referenced by the nav entries (e.g.,
index.md; getting-started/install.md, getting-started/quickstart.md,
getting-started/concepts.md; cloud/overview.md, cloud/quickstart.md,
cloud/api.md, cloud/marketplace.md; guides/training.md,
guides/custom-domains.md, guides/mcp.md, guides/agent-graduation.md;
api/brain.md, api/cli.md, api/patterns.md; architecture/overview.md,
architecture/patterns.md, architecture/enhancements.md, architecture/brain.md,
architecture/agent-graduation.md) with at least a frontmatter/title and
placeholder content so mkdocs can build, ensuring file names and directory
structure match the nav keys (e.g., "Getting Started" => getting-started/*,
"Gradata Cloud" => cloud/*, etc.).


plugins:
- search
- mkdocstrings:
handlers:
python:
paths: [src]
options:
show_source: false
show_root_heading: true
heading_level: 2
Comment on lines +58 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that Python source files exist at the configured path

if [[ -d "sdk/src" ]]; then
  echo "✅ sdk/src directory exists"
  echo ""
  echo "Python files found:"
  fd -e py . sdk/src -x echo "  - {}"
  
  # Check if there are any Python files
  if [[ $(fd -e py . sdk/src | wc -l) -eq 0 ]]; then
    echo ""
    echo "⚠️  No Python files found in sdk/src"
    exit 1
  fi
else
  echo "❌ sdk/src directory does not exist"
  exit 1
fi

Repository: Gradata/gradata

Length of output: 93


🏁 Script executed:

# Find Python files and directory structure in the sdk directory
find sdk -type f -name "*.py" | head -20

Repository: Gradata/gradata

Length of output: 41


🏁 Script executed:

# Check the overall structure of the sdk directory
ls -la sdk/

Repository: Gradata/gradata

Length of output: 232


🏁 Script executed:

# Search for common Python package locations
fd -t d "^(src|python|lib|packages)$" sdk/

Repository: Gradata/gradata

Length of output: 41


🏁 Script executed:

# Search for Python files across the entire repository
find . -type f -name "*.py" -not -path "*/\.*" | head -20

Repository: Gradata/gradata

Length of output: 636


🏁 Script executed:

# Check if there's a src directory at repository root
ls -la | grep -E "^d.*src$|^d.*python"

Repository: Gradata/gradata

Length of output: 112


🏁 Script executed:

# Check the overall repository structure at root
ls -la

Repository: Gradata/gradata

Length of output: 1270


🏁 Script executed:

# Check the root mkdocs.yml to understand the documentation structure
cat mkdocs.yml | head -30

Repository: Gradata/gradata

Length of output: 758


🏁 Script executed:

# Check if there's a build script or CI configuration that runs mkdocs
find . -name "*.yml" -o -name "*.yaml" | xargs grep -l "mkdocs" 2>/dev/null

Repository: Gradata/gradata

Length of output: 117


🏁 Script executed:

# Check the GitHub workflow to see how mkdocs is executed
cat .github/workflows/docs.yml

Repository: Gradata/gradata

Length of output: 1089


🏁 Script executed:

# Search for any references to sdk/mkdocs.yml in the codebase
rg "sdk.*mkdocs|mkdocs.*sdk" --type yaml --type md --type txt

Repository: Gradata/gradata

Length of output: 150


🏁 Script executed:

# Check if there's any build documentation or README about sdk
grep -r "sdk" README.md CONTRIBUTING.md 2>/dev/null | head -20

Repository: Gradata/gradata

Length of output: 41


🏁 Script executed:

# Try a simpler search for sdk references
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*.yml" \) ! -path "*/.git/*" -exec grep -l "sdk" {} \;

Repository: Gradata/gradata

Length of output: 119


🏁 Script executed:

# Check if there are any files inside the sdk directory beyond mkdocs.yml
find sdk -type f

Repository: Gradata/gradata

Length of output: 73


🏁 Script executed:

# Check git status of sdk directory to understand its state
git ls-files sdk/

Repository: Gradata/gradata

Length of output: 73


🏁 Script executed:

# Check the SDK CI workflow to see if it uses mkdocs
cat .github/workflows/sdk-ci.yml

Repository: Gradata/gradata

Length of output: 1858


🏁 Script executed:

# Check if there's a pyproject.toml in the sdk directory
cat sdk/pyproject.toml 2>/dev/null || echo "No pyproject.toml in sdk/"

Repository: Gradata/gradata

Length of output: 84


🏁 Script executed:

# List all files in the sdk directory recursively
find sdk -type f

Repository: Gradata/gradata

Length of output: 73


Fix the SDK source path configuration — the mkdocstrings path is incorrect, and the broader SDK CI will fail.

The paths: [src] in mkdocstrings is configured to look for sdk/src, but this directory does not exist. The actual Python source is located at ../src relative to the sdk directory (or ./src from the repository root). Additionally, the SDK CI workflow (.github/workflows/sdk-ci.yml) also references src/gradata/ from the sdk directory, which means both the documentation and CI tasks will fail.

Update paths: [../src] in sdk/mkdocs.yml to correctly reference the Python source location, or reconfigure the SDK directory structure if it should be independent.

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

In `@sdk/mkdocs.yml` around lines 58 - 67, The mkdocstrings handler in mkdocs
config currently points to the wrong source (paths: [src]) causing docs/CI to
look for sdk/src; update the mkdocstrings paths setting (under the mkdocstrings
-> handlers -> python -> paths key) to paths: [../src] so it correctly
references the repository Python package (or alternatively adjust SDK layout),
and ensure this matches how the SDK CI references src/gradata so both docs
rendering and CI use the same ../src location.


markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true
23 changes: 23 additions & 0 deletions src/gradata/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,29 @@ def brain_correct(
except Exception as e:
_log.debug("Domain fire attribution failed: %s", e)

# Self-healing: detect rule failures
try:
from gradata.enhancements.self_healing import detect_rule_failure

all_lessons = brain._load_lessons()
failure = detect_rule_failure(
lessons=all_lessons,
correction_category=category or "UNKNOWN",
correction_description=desc or summary or "",
)
if failure:
failure["correction_event_id"] = event.get("id")
failure["correction_severity"] = diff.severity
brain.emit(
"RULE_FAILURE", "brain.correct:self_healing",
failure,
[f"category:{failure['failed_rule_category']}", "self_healing"],
session,
)
event["rule_failure_detected"] = True
except Exception as e:
_log.debug("Self-healing detection failed: %s", e)

# Persist rule graph
if hasattr(brain, '_rule_graph') and brain._rule_graph:
with contextlib.suppress(Exception):
Expand Down
43 changes: 43 additions & 0 deletions src/gradata/brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,48 @@ def correct(self, draft: str, final: str, category: str | None = None,
approval_required=approval_required, dry_run=dry_run,
min_severity=min_severity, scope=scope)

def patch_rule(self, category: str, old_description: str, new_description: str,
reason: str = "") -> dict:
"""Rewrite a rule's description. Preserves confidence/metadata. Emits RULE_PATCHED event."""
from gradata._db import write_lessons_safe
from gradata.enhancements.self_healing import apply_patch
from gradata.enhancements.self_improvement import format_lessons, parse_lessons

# Only patch the brain-local lessons file — never external fallbacks
lessons_path = self.dir / "lessons.md"
if not lessons_path.is_file():
return {"patched": False, "error": "not_found: no brain-local lessons file"}

lessons = parse_lessons(lessons_path.read_text(encoding="utf-8"))
patched = apply_patch(lessons, category, old_description, new_description)

if not patched:
return {"patched": False, "error": f"not_found: no rule matching category={category!r}"}

write_lessons_safe(lessons_path, format_lessons(lessons))

# Re-sign the patched rule so HMAC verification stays valid
try:
from gradata.enhancements.rule_integrity import sign_and_store
sign_and_store(self.db_path, new_description, category, patched.confidence)
except ImportError:
pass # unsigned mode — no-op

self.emit("RULE_PATCHED", "brain.patch_rule", {
"category": category,
"old_description": old_description[:200],
"new_description": new_description[:200],
"reason": reason,
"confidence_preserved": patched.confidence,
}, [f"category:{category}", "self_healing"])

return {
"patched": True,
"old_description": old_description,
"new_description": new_description,
"confidence_preserved": patched.confidence,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

def end_session(self, session_corrections: list[dict] | None = None,
session_type: str = "full", machine_mode: bool | None = None,
skip_meta_rules: bool = False) -> dict:
Expand Down Expand Up @@ -665,6 +707,7 @@ def review_pending(self) -> list[dict]:
import sqlite3

from gradata._db import get_connection

conn = get_connection(self.db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute(
Expand Down
6 changes: 4 additions & 2 deletions src/gradata/enhancements/rule_canary.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def _get_db_path(ctx=None) -> Path:
return p

# 3. Relative traversal (SDK installed alongside brain)
# Expects: src/gradata/enhancements/rule_canary.py -> 5 parents -> repo/brain/
try:
scripts_dir = Path(__file__).resolve().parent.parent.parent.parent.parent / "brain"
p = scripts_dir / "system.db"
Expand Down Expand Up @@ -148,8 +149,9 @@ def check_canary_health(rule_category: str, session: int, db_path: Path | None =
try:
corr_row = conn.execute(
"SELECT COUNT(*) as cnt FROM events WHERE type = 'CORRECTION' "
"AND data_json LIKE ? AND CAST(session AS INTEGER) >= ?",
(f'%"{rule_category}"%', start_session),
"AND json_extract(data_json, '$.rule_category') = ? "
"AND CAST(session AS INTEGER) >= ?",
(rule_category, start_session),
Comment on lines +152 to +154
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Wrong JSON field name silently disables rollback detection

The fix correctly moves away from LIKE, but uses the field name $.rule_category, which does not exist in CORRECTION event payloads. _core.py (line 166) stores the correction category under the key "category":

data = {
    ...
    "category": category or "UNKNOWN",
    ...
}

Because json_extract(data_json, '$.rule_category') will always return NULL for every CORRECTION event, correction_count will permanently be 0, and no canary rule will ever be recommended for ROLLBACK — even when a new rule is actively causing regressions. The canary's entire protective function is silently defeated.

Suggested change
"AND json_extract(data_json, '$.rule_category') = ? "
"AND CAST(session AS INTEGER) >= ?",
(rule_category, start_session),
"AND json_extract(data_json, '$.category') = ? "
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gradata/enhancements/rule_canary.py
Line: 152-154

Comment:
**Wrong JSON field name silently disables rollback detection**

The fix correctly moves away from `LIKE`, but uses the field name `$.rule_category`, which does not exist in `CORRECTION` event payloads. `_core.py` (line 166) stores the correction category under the key `"category"`:

```python
data = {
    ...
    "category": category or "UNKNOWN",
    ...
}
```

Because `json_extract(data_json, '$.rule_category')` will always return `NULL` for every CORRECTION event, `correction_count` will permanently be 0, and no canary rule will ever be recommended for `ROLLBACK` — even when a new rule is actively causing regressions. The canary's entire protective function is silently defeated.

```suggestion
                    "AND json_extract(data_json, '$.category') = ? "
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

).fetchone()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if corr_row:
correction_count = corr_row["cnt"]
Expand Down
9 changes: 5 additions & 4 deletions src/gradata/enhancements/rule_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ def sign_lesson_file(lessons_path: Path) -> dict[str, str]:
Parses lesson lines matching [STATE:CONF] CATEGORY: description
and returns a {category: signature} map.

Note: Only the *last* rule per category is retained. This is intentional —
graduation produces one canonical rule per category, so duplicates indicate
stale entries. The returned dict is keyed by category for O(1) lookup.

Args:
lessons_path: Path to lessons.md file.

Expand Down Expand Up @@ -208,8 +212,7 @@ def verify_lesson_file(lessons_path: Path, signatures: dict[str, str]) -> list[s

def _ensure_table(db_path: Path) -> None:
"""Create rule_signatures table if it doesn't exist."""
conn = sqlite3.connect(str(db_path))
try:
with sqlite3.connect(str(db_path)) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS rule_signatures (
category TEXT PRIMARY KEY,
Expand All @@ -218,8 +221,6 @@ def _ensure_table(db_path: Path) -> None:
)
""")
conn.commit()
finally:
conn.close()


def store_signature(db_path: Path, category: str, signature: str) -> None:
Expand Down
Loading
Loading