Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<a href="#quick-start">
<img src="https://img.shields.io/badge/llm-claude%20code%20cli-purple.svg" alt="LLM">
</a>
<a href="https://discord.gg/Jbft3jPn">
<img src="https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white" alt="Discord">
</a>
</p>

<p align="center">
Expand Down Expand Up @@ -58,9 +61,9 @@ Four ways this changes what Claude Code can do for you:

- 💡 **Stop repeating the same mistakes:** Produces actionable playbooks Claude can follow next time; memory only records what happened.

> *Example:* you tell Claude to stop running `npm test` without `--run` because watch mode hangs.<br>
> **Memory:** “user was annoyed about npm test hanging”<br>
> **Learning:** “always pass `--run` to `npm test` in this repodefault watch mode blocks CI
> *Example:* a deploy fails after Claude bumps `prisma` from 5.x to 6.0; you tell it to roll back because 6.0 breaks nested writes in your order flow.<br>
> **Memory:** “deploy broke after prisma bump; user rolled back”<br>
> **Learning:** “treat major-version bumps of ORMs/DB drivers as breakingverify with integration tests, not just unit tests

- 🚀 **Start from the optimized path:** Preserves and optimizes execution paths so Claude can reuse what already works.
> *Example:* Claude spends several iterations trying to start the local dev environment before discovering that this repo requires `pnpm dev:all` instead of the usual `npm run dev`.<br>
Expand Down
Binary file modified assets/playbook_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/profile_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions plugin/dashboard/app/configure/server/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ export default function ConfigureServerPage() {
<Input
type="number"
min={1}
value={srvConfig.batch_size ?? ""}
value={srvConfig.window_size ?? ""}
onChange={(e) => {
const n = Number(e.target.value);
updateSrv(
"batch_size",
"window_size",
Number.isFinite(n) && n > 0 ? n : undefined,
);
}}
Expand All @@ -184,11 +184,11 @@ export default function ConfigureServerPage() {
<Input
type="number"
min={1}
value={srvConfig.batch_interval ?? ""}
value={srvConfig.stride_size ?? ""}
onChange={(e) => {
const n = Number(e.target.value);
updateSrv(
"batch_interval",
"stride_size",
Number.isFinite(n) && n > 0 ? n : undefined,
);
}}
Expand Down
2 changes: 1 addition & 1 deletion plugin/dashboard/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export default function DashboardPage() {
<EmptyState
icon={BookOpen}
title="No playbooks yet"
description="Playbooks are extracted from your interactionsrun /smart-sync to force extraction."
description="Keep using Claude with claude-smart enabledplaybooks will be extracted automatically from your interactions when patterns emerge."
/>
)}
</section>
Expand Down
2 changes: 1 addition & 1 deletion plugin/dashboard/app/playbooks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default function PlaybooksPage() {
<EmptyState
icon={BookOpen}
title="No playbooks match"
description="Adjust the filters or run /smart-sync to extract rules from recent interactions."
description="Adjust the filters, or keep using Claude with claude-smart enabled — playbook rules will be extracted automatically from your interactions when patterns emerge."
/>
) : (
<div className="grid gap-3 lg:grid-cols-2">
Expand Down
2 changes: 1 addition & 1 deletion plugin/dashboard/app/profiles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function ProfilesPage() {
<EmptyState
icon={Users}
title="No profiles yet"
description="Profiles are generated from interactions once the extractor runs. Try /smart-sync after a few turns."
description="Keep using Claude with claude-smart enabled — profiles will appear here automatically as the extractor learns patterns from your interactions."
/>
) : (
<div className="grid gap-3 md:grid-cols-2">
Expand Down
4 changes: 2 additions & 2 deletions plugin/dashboard/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export interface ReflexioExtractorConfig {

export interface ReflexioConfig {
agent_context_prompt?: string | null;
batch_size?: number;
batch_interval?: number;
window_size?: number;
stride_size?: number;
profile_extractor_configs?: ReflexioExtractorConfig[] | null;
user_playbook_extractor_configs?: ReflexioExtractorConfig[] | null;
[k: string]: unknown;
Expand Down
2 changes: 1 addition & 1 deletion plugin/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Self-improving Claude Code plugin — learns from corrections via
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"reflexio-ai>=0.2.15",
"reflexio-ai>=0.2.20",
# Used by reflexio's local embedding provider (ONNXMiniLM_L6_V2).
# Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
# downloaded on first use, not at install time.
Expand Down
14 changes: 7 additions & 7 deletions plugin/src/claude_smart/events/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
from claude_smart import context_format, cs_cite, hook, ids, state
from claude_smart.reflexio_adapter import Adapter

# Claude-smart's preferred extraction cadence — more frequent, smaller batches
# Claude-smart's preferred extraction cadence — more frequent, smaller windows
# than reflexio's out-of-box 10/5. Applied idempotently to the reflexio server
# on every SessionStart via Adapter.apply_batch_defaults.
_CLAUDE_SMART_BATCH_SIZE = 5
_CLAUDE_SMART_BATCH_INTERVAL = 3
# on every SessionStart via Adapter.apply_extraction_defaults.
_CLAUDE_SMART_WINDOW_SIZE = 5
_CLAUDE_SMART_STRIDE_SIZE = 3


def handle(payload: dict[str, Any]) -> None:
Expand All @@ -26,9 +26,9 @@ def handle(payload: dict[str, Any]) -> None:

project_id = ids.resolve_project_id(cwd)
adapter = Adapter()
adapter.apply_batch_defaults(
batch_size=_CLAUDE_SMART_BATCH_SIZE,
batch_interval=_CLAUDE_SMART_BATCH_INTERVAL,
adapter.apply_extraction_defaults(
window_size=_CLAUDE_SMART_WINDOW_SIZE,
stride_size=_CLAUDE_SMART_STRIDE_SIZE,
)
playbooks, profiles = adapter.fetch_both(
project_id=project_id,
Expand Down
22 changes: 11 additions & 11 deletions plugin/src/claude_smart/reflexio_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,23 @@ def delete_all(self) -> tuple[dict[str, int], list[tuple[str, str]]] | None:
counts[name] = getattr(response, "deleted_count", 0) or 0
return counts, errors

def apply_batch_defaults(self, *, batch_size: int, batch_interval: int) -> bool:
"""Push claude-smart's preferred batch defaults to the reflexio server.
def apply_extraction_defaults(self, *, window_size: int, stride_size: int) -> bool:
"""Push claude-smart's preferred extraction defaults to the reflexio server.

Reads the current ``Config`` and only issues a ``set_config`` when the
server-side values differ, so steady state is a single cheap GET.

Reflexio persists ``Config`` to disk, so once these values land they
survive backend restarts. The flip side: if an operator customizes
``batch_size``/``batch_interval`` via the dashboard, this call will
``window_size``/``stride_size`` via the dashboard, this call will
overwrite those values back to the claude-smart defaults on the next
SessionStart. To change the defaults, edit the constants at the call
site in ``events/session_start.py``.

Args:
batch_size (int): Desired ``Config.batch_size`` on the server.
batch_interval (int): Desired ``Config.batch_interval`` on the
server. Must be ``<= batch_size`` (reflexio enforces this).
window_size (int): Desired ``Config.window_size`` on the server.
stride_size (int): Desired ``Config.stride_size`` on the
server. Must be ``<= window_size`` (reflexio enforces this).

Returns:
bool: True if the server is already at the target values or the
Expand All @@ -150,16 +150,16 @@ def apply_batch_defaults(self, *, batch_size: int, batch_interval: int) -> bool:
try:
config = client.get_config()
if (
getattr(config, "batch_size", None) == batch_size
and getattr(config, "batch_interval", None) == batch_interval
getattr(config, "window_size", None) == window_size
and getattr(config, "stride_size", None) == stride_size
):
return True
config.batch_size = batch_size
config.batch_interval = batch_interval
config.window_size = window_size
config.stride_size = stride_size
client.set_config(config)
return True
except Exception as exc: # noqa: BLE001 — adapter must never raise.
_LOGGER.warning("apply_batch_defaults failed: %s", exc)
_LOGGER.warning("apply_extraction_defaults failed: %s", exc)
return False

# -----------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion plugin/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion reflexio
Submodule reflexio updated 49 files
+2 −2 docs/components/configure/config-editor.tsx
+23 −23 docs/components/configure/sections.tsx
+19 −19 docs/lib/config-schema.ts
+2 −2 notebooks/06_real_world_simulation.ipynb
+3 −3 notebooks/07_langchain_integration.ipynb
+1 −1 notebooks/_display_helpers.py
+1 −1 reflexio/benchmarks/retrieval_latency/bench.py
+1 −1 reflexio/cli/README.md
+1 −1 reflexio/cli/commands/interactions.py
+2 −2 reflexio/cli/commands/shortcuts.py
+4 −4 reflexio/client/client.py
+1 −1 reflexio/models/api_schema/domain/entities.py
+84 −38 reflexio/models/config_schema.py
+8 −8 reflexio/server/README.md
+9 −5 reflexio/server/api.py
+128 −34 reflexio/server/services/base_generation_service.py
+9 −1 reflexio/server/services/configurator/base_configurator.py
+46 −46 reflexio/server/services/extractor_interaction_utils.py
+64 −4 reflexio/server/services/generation_service.py
+1 −1 reflexio/server/services/playbook/README.md
+90 −4 reflexio/server/services/playbook/playbook_aggregator.py
+10 −12 reflexio/server/services/playbook/playbook_extractor.py
+4 −4 reflexio/server/services/playbook/playbook_generation_service.py
+2 −2 reflexio/server/services/playbook/playbook_service_utils.py
+9 −11 reflexio/server/services/profile/profile_extractor.py
+5 −5 reflexio/server/services/profile/profile_generation_service.py
+2 −2 reflexio/server/services/profile/profile_generation_service_utils.py
+7 −7 reflexio/server/services/reflection/reflection_service.py
+2 −2 reflexio/server/services/reflection/reflection_service_utils.py
+113 −0 reflexio/server/usage_metrics.py
+6 −6 tests/e2e_tests/conftest.py
+4 −4 tests/e2e_tests/test_openclaw_integration.py
+2 −2 tests/e2e_tests/test_playbook_workflows.py
+2 −2 tests/e2e_tests/test_profile_workflows.py
+1 −1 tests/lib/test_profile_workflows_unit.py
+121 −25 tests/models/test_validators.py
+2 −2 tests/server/api_endpoints/test_api_routes.py
+5 −5 tests/server/services/configurator/test_config_storage_contract.py
+3 −3 tests/server/services/playbook/test_playbook_extractor.py
+11 −11 tests/server/services/playbook/test_playbook_generation_service.py
+2 −2 tests/server/services/playbook/test_playbook_generation_service_integration.py
+3 −3 tests/server/services/profile/test_profile_extractor.py
+6 −6 tests/server/services/reflection/test_reflection_service.py
+36 −38 tests/server/services/test_base_generation_service.py
+67 −101 tests/server/services/test_extractor_interaction_utils.py
+2 −2 tests/server/services/test_generation_service.py
+16 −16 tests/server/services/test_profile_generation_service.py
+8 −8 tests/server/services/test_profile_source_filtering.py
+4 −4 tests/test_data/scenarios/eval_config.json
36 changes: 17 additions & 19 deletions tests/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,17 +229,15 @@ def test_fetch_playbooks_default_top_k_is_tightened() -> None:


# -----------------------------------------------------------------------------
# apply_batch_defaults — push claude-smart's preferred cadence to reflexio
# apply_extraction_defaults — push claude-smart's preferred cadence to reflexio
# -----------------------------------------------------------------------------


class _ConfigClient:
"""Captures get_config/set_config calls against a mutable config object."""

def __init__(self, *, batch_size: int, batch_interval: int, get_raises=None):
self._config = SimpleNamespace(
batch_size=batch_size, batch_interval=batch_interval
)
def __init__(self, *, window_size: int, stride_size: int, get_raises=None):
self._config = SimpleNamespace(window_size=window_size, stride_size=stride_size)
self._get_raises = get_raises
self.set_calls: list[SimpleNamespace] = []

Expand All @@ -251,39 +249,39 @@ def get_config(self):
def set_config(self, config):
self.set_calls.append(
SimpleNamespace(
batch_size=config.batch_size, batch_interval=config.batch_interval
window_size=config.window_size, stride_size=config.stride_size
)
)
self._config = config
return {"ok": True}


def test_apply_batch_defaults_writes_when_values_differ() -> None:
client = _ConfigClient(batch_size=10, batch_interval=5)
def test_apply_extraction_defaults_writes_when_values_differ() -> None:
client = _ConfigClient(window_size=10, stride_size=5)
a = _adapter_with(client)
assert a.apply_batch_defaults(batch_size=5, batch_interval=3) is True
assert a.apply_extraction_defaults(window_size=5, stride_size=3) is True
assert len(client.set_calls) == 1
assert client.set_calls[0].batch_size == 5
assert client.set_calls[0].batch_interval == 3
assert client.set_calls[0].window_size == 5
assert client.set_calls[0].stride_size == 3


def test_apply_batch_defaults_skips_set_when_values_match() -> None:
client = _ConfigClient(batch_size=5, batch_interval=3)
def test_apply_extraction_defaults_skips_set_when_values_match() -> None:
client = _ConfigClient(window_size=5, stride_size=3)
a = _adapter_with(client)
assert a.apply_batch_defaults(batch_size=5, batch_interval=3) is True
assert a.apply_extraction_defaults(window_size=5, stride_size=3) is True
assert client.set_calls == []


def test_apply_batch_defaults_returns_false_when_client_unavailable(
def test_apply_extraction_defaults_returns_false_when_client_unavailable(
monkeypatch,
) -> None:
a = reflexio_adapter.Adapter()
monkeypatch.setattr(a, "_get_client", lambda: None)
assert a.apply_batch_defaults(batch_size=5, batch_interval=3) is False
assert a.apply_extraction_defaults(window_size=5, stride_size=3) is False


def test_apply_batch_defaults_absorbs_get_config_errors() -> None:
def test_apply_extraction_defaults_absorbs_get_config_errors() -> None:
a = _adapter_with(
_ConfigClient(batch_size=10, batch_interval=5, get_raises=RuntimeError("down"))
_ConfigClient(window_size=10, stride_size=5, get_raises=RuntimeError("down"))
)
assert a.apply_batch_defaults(batch_size=5, batch_interval=3) is False
assert a.apply_extraction_defaults(window_size=5, stride_size=3) is False
14 changes: 7 additions & 7 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ def test_session_start_emits_continue_when_no_playbook_or_profile(
session_dir, monkeypatch
) -> None:
class StubAdapter:
def apply_batch_defaults(self, **_kw):
def apply_extraction_defaults(self, **_kw):
return True

def fetch_both(self, **_kw):
Expand All @@ -1230,7 +1230,7 @@ def test_session_start_emits_additional_context_when_playbook_present(
session_dir, monkeypatch
) -> None:
class Stub:
def apply_batch_defaults(self, **_kw):
def apply_extraction_defaults(self, **_kw):
return True

def fetch_both(self, **_kw):
Expand Down Expand Up @@ -1260,14 +1260,14 @@ def fetch_both(self, **_kw):
assert "use pathlib" in payload["hookSpecificOutput"]["additionalContext"]


def test_session_start_applies_claude_smart_batch_defaults(
def test_session_start_applies_claude_smart_extraction_defaults(
session_dir, monkeypatch
) -> None:
"""SessionStart must push claude-smart's 5/3 batch defaults to reflexio."""
"""SessionStart must push claude-smart's 5/3 extraction defaults to reflexio."""
applied: list[dict[str, Any]] = []

class Stub:
def apply_batch_defaults(self, **kwargs):
def apply_extraction_defaults(self, **kwargs):
applied.append(kwargs)
return True

Expand All @@ -1284,15 +1284,15 @@ def fetch_both(self, **_kw):
buf = io.StringIO()
monkeypatch.setattr(sys, "stdout", buf)
session_start.handle({"session_id": "s1", "source": "startup"})
assert applied == [{"batch_size": 5, "batch_interval": 3}]
assert applied == [{"window_size": 5, "stride_size": 3}]


def test_session_start_fetches_both_on_every_source(session_dir, monkeypatch) -> None:
"""Used to skip profile fetch unless source ∈ {resume,clear,compact}; now always both."""
calls: list[dict[str, Any]] = []

class Stub:
def apply_batch_defaults(self, **_kw):
def apply_extraction_defaults(self, **_kw):
return True

def fetch_both(self, **kwargs):
Expand Down
Loading