openreflex 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openreflex-0.9.2/src/openreflex.egg-info → openreflex-0.9.4}/PKG-INFO +1 -1
- {openreflex-0.9.2 → openreflex-0.9.4}/pyproject.toml +1 -1
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/__init__.py +1 -1
- openreflex-0.9.4/src/openreflex/claude_ui.py +186 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/cli.py +8 -2
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/entrypoint.py +30 -18
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/mcp_server.py +11 -9
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/metrics.py +12 -4
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/reflex_index.py +9 -2
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/reflexes.py +76 -12
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/tui.py +3 -2
- {openreflex-0.9.2 → openreflex-0.9.4/src/openreflex.egg-info}/PKG-INFO +1 -1
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/SOURCES.txt +2 -0
- openreflex-0.9.4/tests/test_claude_ui.py +152 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_mcp_metrics_benchmark.py +3 -1
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_reflexes.py +61 -2
- openreflex-0.9.4/tests/test_project_root_reflex.py +57 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_tui_memory.py +11 -11
- openreflex-0.9.2/src/openreflex/claude_ui.py +0 -77
- {openreflex-0.9.2 → openreflex-0.9.4}/LICENSE +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/README.md +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/setup.cfg +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/__main__.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/benchmark.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/control.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/decision.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/detect.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/engine.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/execution_state.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/hooks.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/install.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/integrations/opencode.ts +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/learning.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/mcp_contracts.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/models.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/policies/default.toml +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/policy.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/privacy.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project_map.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project_memory.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/routing.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/store.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/updater.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/usage.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/dependency_links.txt +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/entry_points.txt +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/requires.txt +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/top_level.txt +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_completion_recap_semantics.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_engine.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_execution_state.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_hooks.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_install_cli.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_live_eval.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_outcome_closure.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_policy_recap.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_primer_background.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_map.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_memory.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_scope.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_quality_hardening.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_reflex_index.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_release_consistency.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_release_pipeline.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_reliability_lifecycle.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_routing_control.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_stress.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_token_receiver.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_tokens_cli.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_update_cli.py +0 -0
- {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_work_modes_tokens.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openreflex"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.4"
|
|
8
8
|
description = "Ambient project muscle memory for AI coding agents: learns and reuses project Reflexes automatically"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Claude Code presentation integration.
|
|
2
|
+
|
|
3
|
+
OpenReflex owns a user-level status line, never a project-level statusLine entry.
|
|
4
|
+
Legacy OpenReflex project entries are migrated away without touching user-owned settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
LEGACY_STATUSLINE_COMMAND = "openreflex statusline"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def statusline_command() -> str:
|
|
17
|
+
"""Return the runtime-owned statusline command.
|
|
18
|
+
|
|
19
|
+
Plugin-managed runtimes pass OPENREFLEX_RUNTIME_COMMAND so the status line cannot
|
|
20
|
+
resolve an unrelated or stale global `openreflex` executable from PATH.
|
|
21
|
+
"""
|
|
22
|
+
runtime = os.environ.get("OPENREFLEX_RUNTIME_COMMAND", "").strip()
|
|
23
|
+
return f"{runtime} statusline" if runtime else LEGACY_STATUSLINE_COMMAND
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def claude_settings_path() -> Path:
|
|
27
|
+
"""Return Claude Code's user-level settings path.
|
|
28
|
+
|
|
29
|
+
CLAUDE_CONFIG_DIR is respected so alternate Claude profiles and isolated tests do
|
|
30
|
+
not fall back to the real user home directory.
|
|
31
|
+
"""
|
|
32
|
+
configured = os.environ.get("CLAUDE_CONFIG_DIR", "").strip()
|
|
33
|
+
root = Path(configured).expanduser() if configured else Path.home() / ".claude"
|
|
34
|
+
return root / "settings.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ours(command: object) -> bool:
|
|
38
|
+
if not isinstance(command, str):
|
|
39
|
+
return False
|
|
40
|
+
value = command.replace("\\", "/").lower()
|
|
41
|
+
return command == LEGACY_STATUSLINE_COMMAND or (
|
|
42
|
+
".openreflex/runtime/" in value and value.rstrip().endswith(" statusline")
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _owned_project_hook(entry: object) -> bool:
|
|
47
|
+
"""True only for hook entries installed by OpenReflex into project settings."""
|
|
48
|
+
value = json.dumps(entry, default=str).replace("\\\\", "/").lower()
|
|
49
|
+
return (
|
|
50
|
+
"openreflex hook claude-code" in value
|
|
51
|
+
or ("runtime/launcher.cjs" in value and " hook claude-code " in value)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load(path: Path) -> dict | None:
|
|
56
|
+
if not path.exists():
|
|
57
|
+
return {}
|
|
58
|
+
try:
|
|
59
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
60
|
+
except (OSError, ValueError):
|
|
61
|
+
return None
|
|
62
|
+
return value if isinstance(value, dict) else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _write(path: Path, data: dict) -> None:
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
temp = path.with_suffix(path.suffix + ".tmp")
|
|
68
|
+
temp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
69
|
+
os.replace(temp, path)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _remove_owned_statusline(path: Path, *, delete_empty: bool = False) -> bool:
|
|
73
|
+
data = _load(path)
|
|
74
|
+
if data is None:
|
|
75
|
+
return False
|
|
76
|
+
current = data.get("statusLine")
|
|
77
|
+
if not isinstance(current, dict) or not _ours(current.get("command")):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
data.pop("statusLine", None)
|
|
81
|
+
if delete_empty and not data:
|
|
82
|
+
try:
|
|
83
|
+
path.unlink()
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
pass
|
|
86
|
+
try:
|
|
87
|
+
path.parent.rmdir()
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
else:
|
|
91
|
+
_write(path, data)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def remove_project_integration(project: Path) -> bool:
|
|
96
|
+
"""Remove legacy OpenReflex-owned Claude project config, preserving user config.
|
|
97
|
+
|
|
98
|
+
The Claude plugin is user-scoped and must not require repository-local Claude
|
|
99
|
+
settings. Older OpenReflex releases could leave a project statusLine or hook entries
|
|
100
|
+
behind; remove only those entries and delete the file/directory when it becomes empty.
|
|
101
|
+
"""
|
|
102
|
+
path = project / ".claude" / "settings.json"
|
|
103
|
+
data = _load(path)
|
|
104
|
+
if data is None or not path.exists():
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
changed = False
|
|
108
|
+
current = data.get("statusLine")
|
|
109
|
+
if isinstance(current, dict) and _ours(current.get("command")):
|
|
110
|
+
data.pop("statusLine", None)
|
|
111
|
+
changed = True
|
|
112
|
+
|
|
113
|
+
hooks = data.get("hooks")
|
|
114
|
+
if isinstance(hooks, dict):
|
|
115
|
+
cleaned = {}
|
|
116
|
+
for event, entries in hooks.items():
|
|
117
|
+
if not isinstance(entries, list):
|
|
118
|
+
cleaned[event] = entries
|
|
119
|
+
continue
|
|
120
|
+
kept = [entry for entry in entries if not _owned_project_hook(entry)]
|
|
121
|
+
if kept:
|
|
122
|
+
cleaned[event] = kept
|
|
123
|
+
if len(kept) != len(entries):
|
|
124
|
+
changed = True
|
|
125
|
+
if cleaned:
|
|
126
|
+
data["hooks"] = cleaned
|
|
127
|
+
elif "hooks" in data:
|
|
128
|
+
data.pop("hooks", None)
|
|
129
|
+
|
|
130
|
+
if not changed:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
if not data:
|
|
134
|
+
try:
|
|
135
|
+
path.unlink()
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
pass
|
|
138
|
+
try:
|
|
139
|
+
path.parent.rmdir()
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
else:
|
|
143
|
+
_write(path, data)
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def remove_project_statusline(project: Path) -> bool:
|
|
148
|
+
"""Backward-compatible cleanup entry point for legacy callers."""
|
|
149
|
+
return remove_project_integration(project)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def configure_statusline(project: Path | None = None) -> str:
|
|
153
|
+
"""Configure the OpenReflex status line in Claude's user settings.
|
|
154
|
+
|
|
155
|
+
A project argument is accepted because session hooks know the active project. It is
|
|
156
|
+
used only to migrate a legacy OpenReflex-owned project statusLine entry; the desired
|
|
157
|
+
status line is always written to the user scope.
|
|
158
|
+
"""
|
|
159
|
+
if project is not None:
|
|
160
|
+
remove_project_integration(project)
|
|
161
|
+
|
|
162
|
+
path = claude_settings_path()
|
|
163
|
+
data = _load(path)
|
|
164
|
+
if data is None:
|
|
165
|
+
return "preserved-invalid"
|
|
166
|
+
|
|
167
|
+
existing = data.get("statusLine")
|
|
168
|
+
desired = statusline_command()
|
|
169
|
+
if isinstance(existing, dict):
|
|
170
|
+
current = existing.get("command")
|
|
171
|
+
if current == desired:
|
|
172
|
+
return "already-configured"
|
|
173
|
+
if not _ours(current):
|
|
174
|
+
return "preserved-existing"
|
|
175
|
+
data["statusLine"] = {**existing, "command": desired, "padding": 0, "refreshInterval": 2}
|
|
176
|
+
_write(path, data)
|
|
177
|
+
return "updated-runtime"
|
|
178
|
+
|
|
179
|
+
data["statusLine"] = {"type": "command", "command": desired, "padding": 0, "refreshInterval": 2}
|
|
180
|
+
_write(path, data)
|
|
181
|
+
return "configured"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def remove_statusline() -> bool:
|
|
185
|
+
"""Remove only the OpenReflex-owned user-level status line."""
|
|
186
|
+
return _remove_owned_statusline(claude_settings_path())
|
|
@@ -149,8 +149,14 @@ def cmd_status(args) -> int:
|
|
|
149
149
|
print(f"OpenReflex {__version__} - {project}")
|
|
150
150
|
print(f" enabled: {data['enabled']} agents: {', '.join(data['engagement']['agents']) or '-'}")
|
|
151
151
|
print(f" tasks: {data['engagement']['tasks']} experiences: {data['engagement']['experiences']} lessons: {data['lessons']}")
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
reflexes = data["project_reflexes"]
|
|
153
|
+
print(f" project reflex: {reflexes['project_state']} · {reflexes['project_support']} supporting experience(s)")
|
|
154
|
+
specialised = []
|
|
155
|
+
for key in ("learning", "learned", "proven", "stale"):
|
|
156
|
+
count = int(reflexes.get(key, 0) or 0)
|
|
157
|
+
if count:
|
|
158
|
+
specialised.append(f"{count} {key}")
|
|
159
|
+
print(" specialised reflexes: " + (" / ".join(specialised) if specialised else "learning from project evidence"))
|
|
154
160
|
print(f" first session captured: {data['activation']['first_session_captured']} "
|
|
155
161
|
f"seconds to first task: {data['activation']['seconds_to_first_task']}")
|
|
156
162
|
print(f" tasks that used prior experience: {reuse['benefit_rate']}")
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Callable, TypeVar
|
|
19
19
|
|
|
20
20
|
from . import __version__, cli
|
|
21
|
-
from .claude_ui import configure_statusline,
|
|
21
|
+
from .claude_ui import configure_statusline, remove_project_integration
|
|
22
22
|
from .project import approval, approve, log_error, project_root
|
|
23
23
|
from .project_map import enrich_snapshot
|
|
24
24
|
from .project_memory import ensure_background_refresh, load_snapshot, read_state, run_worker, update_state
|
|
@@ -468,18 +468,21 @@ def _compact_overview(project: Path, plugin_root: str | None = None) -> int:
|
|
|
468
468
|
task = state.get("task") if isinstance(state.get("task"), dict) else {}
|
|
469
469
|
reflex_name = str(task.get("reflex") or "").strip()
|
|
470
470
|
|
|
471
|
-
|
|
471
|
+
reflex_state = "cold"
|
|
472
|
+
reflex_support = 0
|
|
472
473
|
if enabled:
|
|
473
474
|
try:
|
|
474
|
-
from .reflexes import
|
|
475
|
+
from .reflexes import reflex_summary
|
|
475
476
|
from .store import Store, database_path
|
|
476
477
|
store = Store(database_path(project))
|
|
477
478
|
try:
|
|
478
|
-
|
|
479
|
+
summary = reflex_summary(store)
|
|
480
|
+
reflex_state = str(summary["project_state"])
|
|
481
|
+
reflex_support = int(summary["project_support"])
|
|
479
482
|
finally:
|
|
480
483
|
store.close()
|
|
481
484
|
except Exception: # noqa: BLE001 - overview must remain available if optional detail fails
|
|
482
|
-
|
|
485
|
+
reflex_state = "learning"
|
|
483
486
|
|
|
484
487
|
state_label = "READY" if enabled and aligned else "SETUP" if not enabled else "ATTENTION"
|
|
485
488
|
print(f"OPENREFLEX {__version__} {state_label}")
|
|
@@ -487,8 +490,11 @@ def _compact_overview(project: Path, plugin_root: str | None = None) -> int:
|
|
|
487
490
|
print(f"Project {project.name}")
|
|
488
491
|
if enabled:
|
|
489
492
|
experiences = int(state.get("experiences", 0) or 0)
|
|
490
|
-
|
|
491
|
-
print(f"
|
|
493
|
+
reflex_label = reflex_state if experiences else "warming"
|
|
494
|
+
print(f"Memory {experiences} experiences · Project Reflex {reflex_label}")
|
|
495
|
+
if reflex_support:
|
|
496
|
+
print(f"Evidence {reflex_support} supporting experience(s)")
|
|
497
|
+
print(f"Current {reflex_name or ('Project Reflex · ' + reflex_label)}")
|
|
492
498
|
else:
|
|
493
499
|
print("Memory not enabled")
|
|
494
500
|
print(f"Runtime {'aligned' if aligned else f'plugin {plugin_version} / runtime {runtime_version}'}")
|
|
@@ -564,8 +570,10 @@ def _onboard(argv: list[str]) -> int:
|
|
|
564
570
|
print(" memory enabled · local")
|
|
565
571
|
if status == "preserved-existing":
|
|
566
572
|
print(" statusline existing user status line preserved")
|
|
573
|
+
elif status == "preserved-invalid":
|
|
574
|
+
print(" statusline user settings unreadable; preserved unchanged")
|
|
567
575
|
else:
|
|
568
|
-
print(" statusline managed OpenReflex runtime")
|
|
576
|
+
print(" statusline managed OpenReflex runtime · user scope")
|
|
569
577
|
print("")
|
|
570
578
|
print(dashboard(project))
|
|
571
579
|
return 0
|
|
@@ -610,33 +618,35 @@ def _memory(argv: list[str]) -> int:
|
|
|
610
618
|
|
|
611
619
|
|
|
612
620
|
def _reflexes(argv: list[str]) -> int:
|
|
613
|
-
from .reflexes import
|
|
621
|
+
from .reflexes import display_reflexes, display_state
|
|
614
622
|
from .store import Store, database_path
|
|
615
623
|
|
|
616
624
|
if argv and argv[0] in {"-h", "--help"}:
|
|
617
625
|
print("usage: openreflex reflexes [--project PATH]\n")
|
|
618
|
-
print("Show
|
|
626
|
+
print("Show the project Reflex and specialised Reflexes, including active learning.")
|
|
619
627
|
return 0
|
|
620
628
|
project = project_root(_project_arg(argv))
|
|
621
629
|
store = Store(database_path(project))
|
|
622
630
|
try:
|
|
623
|
-
reflexes =
|
|
631
|
+
reflexes = display_reflexes(store)
|
|
624
632
|
finally:
|
|
625
633
|
store.close()
|
|
626
634
|
print("OPENREFLEX / PROJECT REFLEXES")
|
|
627
635
|
print(f" project {project}")
|
|
628
636
|
if not reflexes:
|
|
629
|
-
print(" state
|
|
630
|
-
print("
|
|
637
|
+
print(" state warming")
|
|
638
|
+
print(" reflex starts with the first captured experience")
|
|
631
639
|
return 0
|
|
632
640
|
for reflex in reflexes:
|
|
633
641
|
print("")
|
|
634
642
|
print(f" {reflex.name}")
|
|
635
|
-
print(f" state {reflex
|
|
643
|
+
print(f" state {display_state(reflex)}")
|
|
636
644
|
print(f" evidence {reflex.success_count} successful / {reflex.verified_count} verified")
|
|
637
|
-
if reflex.family
|
|
645
|
+
if reflex.family == "project-root":
|
|
646
|
+
print(" scope whole project · cross-task · cross-mode")
|
|
647
|
+
elif reflex.family.startswith("area:"):
|
|
638
648
|
print(" scope cross-task project area")
|
|
639
|
-
|
|
649
|
+
elif reflex.procedure:
|
|
640
650
|
print(f" steps {' -> '.join(reflex.procedure)}")
|
|
641
651
|
return 0
|
|
642
652
|
|
|
@@ -694,14 +704,16 @@ def _augment_install(argv: list[str], result: int) -> None:
|
|
|
694
704
|
print(f" statusline OpenReflex v{__version__} enabled")
|
|
695
705
|
elif status == "preserved-existing":
|
|
696
706
|
print(" statusline existing user status line preserved")
|
|
707
|
+
elif status == "preserved-invalid":
|
|
708
|
+
print(" statusline user settings unreadable; preserved unchanged")
|
|
697
709
|
|
|
698
710
|
|
|
699
711
|
def _augment_uninstall(argv: list[str], result: int) -> None:
|
|
700
712
|
if result != 0 or len(argv) < 2 or argv[1] != "claude-code" or "--dry-run" in argv:
|
|
701
713
|
return
|
|
702
714
|
project = project_root(_project_arg(argv))
|
|
703
|
-
if
|
|
704
|
-
print("
|
|
715
|
+
if remove_project_integration(project):
|
|
716
|
+
print(" project legacy OpenReflex Claude config removed")
|
|
705
717
|
|
|
706
718
|
|
|
707
719
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -20,7 +20,7 @@ from .mcp_contracts import (
|
|
|
20
20
|
SearchExperienceResult,
|
|
21
21
|
)
|
|
22
22
|
from .project import approval, approve, project_root
|
|
23
|
-
from .reflexes import
|
|
23
|
+
from .reflexes import display_reflexes, display_state
|
|
24
24
|
from .routing import Limits
|
|
25
25
|
|
|
26
26
|
INSTRUCTIONS = """OpenReflex runs ambiently through lifecycle hooks. Normal coding-agent work does not require
|
|
@@ -208,26 +208,28 @@ def build_server(project: Path) -> FastMCP:
|
|
|
208
208
|
|
|
209
209
|
@tool("Get project Reflexes", READ)
|
|
210
210
|
def get_project_reflexes() -> str:
|
|
211
|
-
"""Show the project
|
|
212
|
-
Returns:
|
|
213
|
-
|
|
214
|
-
promotes them.
|
|
211
|
+
"""Show the project Reflex and specialised procedures OpenReflex is learning from repeated work.
|
|
212
|
+
Returns: the root Project Reflex after the first captured experience, plus specialised Reflexes with
|
|
213
|
+
learning/learned/proven/stale maturity, support/verification counts and procedure steps.
|
|
215
214
|
Use when: the user asks what OpenReflex has learned specifically about this project, which reusable procedures
|
|
216
215
|
exist, or how a named Reflex works.
|
|
217
216
|
Not for: backend routing internals or generic strategies (use get_candidate_paths for those diagnostics).
|
|
218
217
|
Side effects: none; read-only. It never recompiles memory or touches project files.
|
|
219
218
|
Errors: a 'not enabled' message is returned until the project is approved."""
|
|
220
219
|
def operation(e: Engine):
|
|
221
|
-
reflexes =
|
|
220
|
+
reflexes = display_reflexes(e.store)
|
|
222
221
|
if not reflexes:
|
|
223
|
-
return "OPENREFLEX / PROJECT REFLEXES\n\
|
|
222
|
+
return "OPENREFLEX / PROJECT REFLEXES\n\nProject Reflex warming · capture the first substantial task."
|
|
224
223
|
lines = ["OPENREFLEX / PROJECT REFLEXES", ""]
|
|
225
224
|
for reflex in reflexes:
|
|
226
225
|
lines.append(
|
|
227
|
-
f"{reflex.name} · {reflex.
|
|
226
|
+
f"{reflex.name} · {display_state(reflex)} · {reflex.support_count} supporting · {reflex.success_count} successful · "
|
|
228
227
|
f"{reflex.verified_count} verified"
|
|
229
228
|
)
|
|
230
|
-
|
|
229
|
+
if reflex.family == "project-root":
|
|
230
|
+
lines.append(" scope: whole project · cross-task · cross-mode")
|
|
231
|
+
elif reflex.procedure:
|
|
232
|
+
lines.append(" " + " -> ".join(reflex.procedure))
|
|
231
233
|
return "\n".join(lines)
|
|
232
234
|
return run(operation)
|
|
233
235
|
|
|
@@ -11,6 +11,7 @@ from statistics import mean
|
|
|
11
11
|
|
|
12
12
|
from .engine import Engine
|
|
13
13
|
from .learning import realized_utility
|
|
14
|
+
from .reflexes import reflex_summary
|
|
14
15
|
|
|
15
16
|
WEEK = 604800
|
|
16
17
|
|
|
@@ -47,8 +48,7 @@ def project_metrics(engine: Engine, approval_record: dict | None = None, now: fl
|
|
|
47
48
|
executions = store.list("Execution", limit=100_000)
|
|
48
49
|
experiences = sorted(store.list("Experience", limit=100_000), key=lambda x: x.created_at)
|
|
49
50
|
contexts = {c.task_id: c for c in store.list("Context", limit=100_000)}
|
|
50
|
-
reflexes = store
|
|
51
|
-
reflex_states = Counter(item.state for item in reflexes)
|
|
51
|
+
reflexes = reflex_summary(store)
|
|
52
52
|
task_by_id = {t.id: t for t in tasks}
|
|
53
53
|
approved_at = (approval_record or {}).get("approved_at")
|
|
54
54
|
|
|
@@ -164,7 +164,15 @@ def project_metrics(engine: Engine, approval_record: dict | None = None, now: fl
|
|
|
164
164
|
"execution_control": {"verdicts": dict(verdicts),
|
|
165
165
|
"success_after_pivot_or_stop": _avg(x.status == "success" for x in advised),
|
|
166
166
|
"tasks_within_tool_call_budget": _avg(within_budget)},
|
|
167
|
-
"project_reflexes": {
|
|
168
|
-
|
|
167
|
+
"project_reflexes": {
|
|
168
|
+
"visible": reflexes["visible"],
|
|
169
|
+
"project_state": reflexes["project_state"],
|
|
170
|
+
"project_support": reflexes["project_support"],
|
|
171
|
+
"specialised_total": reflexes["specialised_total"],
|
|
172
|
+
"learning": reflexes["learning"],
|
|
173
|
+
"learned": reflexes["learned"],
|
|
174
|
+
"proven": reflexes["proven"],
|
|
175
|
+
"stale": reflexes["stale"],
|
|
176
|
+
},
|
|
169
177
|
"lessons": len(store.list("Lesson", limit=100_000)),
|
|
170
178
|
}
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
|
|
19
19
|
from .engine import Engine
|
|
20
20
|
from .project_memory import load_snapshot, snapshot_path, update_state
|
|
21
|
+
from .reflexes import reflex_summary
|
|
21
22
|
|
|
22
23
|
ROLE_BONUS = {
|
|
23
24
|
"helm-config": 5.0,
|
|
@@ -91,18 +92,24 @@ def _evidence_state(item: dict) -> str:
|
|
|
91
92
|
return "discovered"
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
def _counts(engine: Engine) -> dict[str,
|
|
95
|
+
def _counts(engine: Engine) -> dict[str, object]:
|
|
95
96
|
outcomes = engine.store.count("Outcome")
|
|
96
97
|
unknown = engine.store.count("Outcome", "status", "unknown")
|
|
98
|
+
reflexes = reflex_summary(engine.store)
|
|
97
99
|
return {
|
|
98
100
|
"experiences": engine.store.count("Experience"),
|
|
99
101
|
"verified": engine.store.count("Outcome", "verified", True),
|
|
100
102
|
"known_outcomes": outcomes - unknown,
|
|
101
103
|
"lessons": engine.store.count("Lesson"),
|
|
104
|
+
"project_reflex_state": reflexes["project_state"],
|
|
105
|
+
"project_reflex_support": reflexes["project_support"],
|
|
106
|
+
"reflexes_learning": reflexes["learning"],
|
|
107
|
+
"reflexes_learned": reflexes["learned"],
|
|
108
|
+
"reflexes_proven": reflexes["proven"],
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
|
|
105
|
-
def sync_counts(project: Path) -> dict[str,
|
|
112
|
+
def sync_counts(project: Path) -> dict[str, object]:
|
|
106
113
|
"""Refresh lightweight UI counts from the local Experience Graph."""
|
|
107
114
|
engine = Engine(project)
|
|
108
115
|
try:
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import math
|
|
7
7
|
import re
|
|
8
|
+
import time
|
|
8
9
|
from collections import Counter
|
|
9
10
|
from pathlib import PurePosixPath
|
|
10
11
|
|
|
@@ -13,6 +14,7 @@ from .routing import embed, similarity
|
|
|
13
14
|
from .store import Store
|
|
14
15
|
|
|
15
16
|
_VISIBLE_STATES = ("learned", "proven")
|
|
17
|
+
PROJECT_ROOT_FAMILY = "project-root"
|
|
16
18
|
_STOPWORDS = {
|
|
17
19
|
"the", "a", "an", "and", "or", "to", "for", "in", "on", "of", "this", "that", "with", "from",
|
|
18
20
|
"please", "project", "repo", "repository", "change", "update", "fix", "add", "make", "work",
|
|
@@ -62,11 +64,13 @@ def _query_hints(text: str) -> set[str]:
|
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def _scope_specificity(family: str) -> int:
|
|
65
|
-
"""Deterministic tie-break: concrete
|
|
67
|
+
"""Deterministic tie-break: concrete scopes beat the always-present project root."""
|
|
66
68
|
if family in {"helm-values", "helm-chart", "helm-template", "terraform", "ci", "database", "dependencies", "tests"}:
|
|
67
69
|
return 3
|
|
68
70
|
if family.startswith("area:"):
|
|
69
71
|
return 2
|
|
72
|
+
if family == PROJECT_ROOT_FAMILY:
|
|
73
|
+
return 0
|
|
70
74
|
return 1
|
|
71
75
|
|
|
72
76
|
|
|
@@ -138,6 +142,9 @@ def families_for_experience(experience: Experience) -> tuple[str, ...]:
|
|
|
138
142
|
|
|
139
143
|
if not families:
|
|
140
144
|
add(f"{experience.task_mode}-{experience.task_class}")
|
|
145
|
+
# Every meaningful execution also reinforces one cross-mode project Reflex.
|
|
146
|
+
# Specialised families remain first so existing primary-family behaviour is preserved.
|
|
147
|
+
add(PROJECT_ROOT_FAMILY)
|
|
141
148
|
return tuple(families)
|
|
142
149
|
|
|
143
150
|
|
|
@@ -147,6 +154,8 @@ def _family(experience: Experience) -> str:
|
|
|
147
154
|
|
|
148
155
|
|
|
149
156
|
def _name(family: str, experience: Experience) -> str:
|
|
157
|
+
if family == PROJECT_ROOT_FAMILY:
|
|
158
|
+
return "Project Reflex"
|
|
150
159
|
if family.startswith("area:"):
|
|
151
160
|
words = family.removeprefix("area:").replace("-", " ")
|
|
152
161
|
return f"{words.capitalize()} work"
|
|
@@ -282,14 +291,14 @@ def _confidence(known: int, successes: int, verified: int) -> float:
|
|
|
282
291
|
|
|
283
292
|
|
|
284
293
|
def _compile_family(store: Store, experience: Experience, family: str, now: float) -> ProjectReflex:
|
|
285
|
-
|
|
286
|
-
|
|
294
|
+
project_scope = family.startswith("area:") or family == PROJECT_ROOT_FAMILY
|
|
295
|
+
observed_group = [
|
|
287
296
|
item for item in store.list("Experience", limit=5000)
|
|
288
|
-
if (
|
|
297
|
+
if (project_scope or item.task_mode == experience.task_mode)
|
|
289
298
|
and family in families_for_experience(item)
|
|
290
|
-
and item.status != "unknown"
|
|
291
299
|
]
|
|
292
|
-
|
|
300
|
+
observed_group.sort(key=lambda item: item.created_at)
|
|
301
|
+
group = [item for item in observed_group if item.status != "unknown"]
|
|
293
302
|
successes = [item for item in group if item.status == "success"]
|
|
294
303
|
verified = 0
|
|
295
304
|
for item in successes:
|
|
@@ -299,7 +308,7 @@ def _compile_family(store: Store, experience: Experience, family: str, now: floa
|
|
|
299
308
|
continue
|
|
300
309
|
verified += int(bool(getattr(outcome, "verified", False)))
|
|
301
310
|
|
|
302
|
-
reflex_mode = "project" if
|
|
311
|
+
reflex_mode = "project" if project_scope else experience.task_mode
|
|
303
312
|
key = f"{reflex_mode}|{family}"
|
|
304
313
|
reflex_id = "reflex-" + hashlib.sha1(key.encode()).hexdigest()[:16]
|
|
305
314
|
try:
|
|
@@ -323,11 +332,11 @@ def _compile_family(store: Store, experience: Experience, family: str, now: floa
|
|
|
323
332
|
task_class=primary_class,
|
|
324
333
|
state=_state(len(group), len(successes), verified, previous_state),
|
|
325
334
|
seed_strategy=seed_strategy,
|
|
326
|
-
# Project-area Reflexes are cross-mode
|
|
327
|
-
#
|
|
328
|
-
procedure=[] if
|
|
329
|
-
evidence_ids=[item.id for item in group[-20:]],
|
|
330
|
-
support_count=len(group),
|
|
335
|
+
# Project-wide and project-area Reflexes are cross-mode memory, not one task recipe.
|
|
336
|
+
# Mode-specific specialised Reflexes keep executable procedures.
|
|
337
|
+
procedure=[] if project_scope else _procedure(store, successes or group or [experience]),
|
|
338
|
+
evidence_ids=[item.id for item in (observed_group if family == PROJECT_ROOT_FAMILY else group)[-20:]],
|
|
339
|
+
support_count=len(observed_group) if family == PROJECT_ROOT_FAMILY else len(group),
|
|
331
340
|
success_count=len(successes),
|
|
332
341
|
verified_count=verified,
|
|
333
342
|
confidence=_confidence(len(group), len(successes), verified),
|
|
@@ -348,6 +357,58 @@ def compile_for_experience(store: Store, experience: Experience, now: float) ->
|
|
|
348
357
|
return compiled[0]
|
|
349
358
|
|
|
350
359
|
|
|
360
|
+
def ensure_project_reflex(store: Store, now: float | None = None) -> ProjectReflex | None:
|
|
361
|
+
"""Backfill/update the root project Reflex from all captured experiences.
|
|
362
|
+
|
|
363
|
+
Older OpenReflex versions did not compile a root project Reflex. Reading a project
|
|
364
|
+
summary is therefore also a safe migration point: existing execution evidence is
|
|
365
|
+
consolidated without inventing success or verification.
|
|
366
|
+
"""
|
|
367
|
+
experiences = sorted(store.list("Experience", limit=5000), key=lambda item: item.created_at)
|
|
368
|
+
if not experiences:
|
|
369
|
+
return None
|
|
370
|
+
latest = experiences[-1]
|
|
371
|
+
return _compile_family(store, latest, PROJECT_ROOT_FAMILY, time.time() if now is None else now)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def display_state(reflex: ProjectReflex) -> str:
|
|
375
|
+
"""User-facing maturity: a candidate means learning is actively underway."""
|
|
376
|
+
return "learning" if reflex.state == "candidate" else reflex.state
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def display_reflexes(store: Store) -> list[ProjectReflex]:
|
|
380
|
+
"""Return the root Reflex plus specialised Reflexes, including learning candidates."""
|
|
381
|
+
root = ensure_project_reflex(store)
|
|
382
|
+
items = store.list_reflexes(limit=500)
|
|
383
|
+
ordered = []
|
|
384
|
+
if root is not None:
|
|
385
|
+
ordered.append(root)
|
|
386
|
+
ordered.extend(
|
|
387
|
+
item for item in items
|
|
388
|
+
if (root is None or item.id != root.id) and item.state in {"candidate", "learned", "proven", "stale"}
|
|
389
|
+
)
|
|
390
|
+
return ordered
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def reflex_summary(store: Store) -> dict[str, object]:
|
|
394
|
+
"""Summarise project learning without collapsing active learning to a misleading zero."""
|
|
395
|
+
items = display_reflexes(store)
|
|
396
|
+
root = next((item for item in items if item.family == PROJECT_ROOT_FAMILY), None)
|
|
397
|
+
specialised = [item for item in items if item.family != PROJECT_ROOT_FAMILY]
|
|
398
|
+
states = Counter(display_state(item) for item in specialised)
|
|
399
|
+
return {
|
|
400
|
+
"project": root,
|
|
401
|
+
"project_state": display_state(root) if root is not None else "cold",
|
|
402
|
+
"project_support": root.support_count if root is not None else 0,
|
|
403
|
+
"visible": len(items),
|
|
404
|
+
"specialised_total": len(specialised),
|
|
405
|
+
"learning": states["learning"],
|
|
406
|
+
"learned": states["learned"],
|
|
407
|
+
"proven": states["proven"],
|
|
408
|
+
"stale": states["stale"],
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
351
412
|
def _reflex_family(reflex: ProjectReflex) -> str:
|
|
352
413
|
if reflex.family:
|
|
353
414
|
return reflex.family
|
|
@@ -379,8 +440,11 @@ def _locality(description: str, reflex: ProjectReflex) -> float:
|
|
|
379
440
|
def match_reflex(store: Store, description: str, task_mode: str, task_class: str,
|
|
380
441
|
threshold: float = 0.28, family_hints: set[str] | None = None) -> tuple[ProjectReflex, float] | None:
|
|
381
442
|
"""Resolve the strongest project Reflex using intent, family, locality and execution evidence."""
|
|
443
|
+
ensure_project_reflex(store)
|
|
382
444
|
query = embed(description)
|
|
383
445
|
hints = _query_hints(description) | set(family_hints or ())
|
|
446
|
+
# The project root is a baseline, not an intent hint; specific area/procedure Reflexes should win.
|
|
447
|
+
hints.discard(PROJECT_ROOT_FAMILY)
|
|
384
448
|
scored: list[tuple[ProjectReflex, float]] = []
|
|
385
449
|
|
|
386
450
|
for reflex in store.list_reflexes(states=_VISIBLE_STATES, limit=500):
|
|
@@ -226,8 +226,9 @@ def dashboard(project: Path, *, colour: bool | None = None) -> str:
|
|
|
226
226
|
f" verified {state.get('verified', 0)}",
|
|
227
227
|
f" lessons {state.get('lessons', 0)}",
|
|
228
228
|
"", "CURRENT REFLEX",
|
|
229
|
-
f" name {task.get('reflex') or '
|
|
230
|
-
f" state {task.get('reflex_state') or '
|
|
229
|
+
f" name {task.get('reflex') or 'Project Reflex'}",
|
|
230
|
+
f" state {task.get('reflex_state') or state.get('project_reflex_state') or ('learning' if state.get('experiences') else 'warming')}",
|
|
231
|
+
f" support {state.get('project_reflex_support', 0)} project experience(s)",
|
|
231
232
|
f" phase {phase.upper()}",
|
|
232
233
|
f" mode {str(task.get('mode') or 'build').upper()}",
|
|
233
234
|
f" recall {recall.get('experiences', 0)} related",
|