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.
Files changed (72) hide show
  1. {openreflex-0.9.2/src/openreflex.egg-info → openreflex-0.9.4}/PKG-INFO +1 -1
  2. {openreflex-0.9.2 → openreflex-0.9.4}/pyproject.toml +1 -1
  3. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/__init__.py +1 -1
  4. openreflex-0.9.4/src/openreflex/claude_ui.py +186 -0
  5. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/cli.py +8 -2
  6. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/entrypoint.py +30 -18
  7. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/mcp_server.py +11 -9
  8. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/metrics.py +12 -4
  9. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/reflex_index.py +9 -2
  10. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/reflexes.py +76 -12
  11. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/tui.py +3 -2
  12. {openreflex-0.9.2 → openreflex-0.9.4/src/openreflex.egg-info}/PKG-INFO +1 -1
  13. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/SOURCES.txt +2 -0
  14. openreflex-0.9.4/tests/test_claude_ui.py +152 -0
  15. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_mcp_metrics_benchmark.py +3 -1
  16. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_reflexes.py +61 -2
  17. openreflex-0.9.4/tests/test_project_root_reflex.py +57 -0
  18. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_tui_memory.py +11 -11
  19. openreflex-0.9.2/src/openreflex/claude_ui.py +0 -77
  20. {openreflex-0.9.2 → openreflex-0.9.4}/LICENSE +0 -0
  21. {openreflex-0.9.2 → openreflex-0.9.4}/README.md +0 -0
  22. {openreflex-0.9.2 → openreflex-0.9.4}/setup.cfg +0 -0
  23. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/__main__.py +0 -0
  24. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/benchmark.py +0 -0
  25. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/control.py +0 -0
  26. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/decision.py +0 -0
  27. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/detect.py +0 -0
  28. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/engine.py +0 -0
  29. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/execution_state.py +0 -0
  30. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/hooks.py +0 -0
  31. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/install.py +0 -0
  32. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/integrations/opencode.ts +0 -0
  33. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/learning.py +0 -0
  34. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/mcp_contracts.py +0 -0
  35. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/models.py +0 -0
  36. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/policies/default.toml +0 -0
  37. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/policy.py +0 -0
  38. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/privacy.py +0 -0
  39. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project.py +0 -0
  40. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project_map.py +0 -0
  41. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/project_memory.py +0 -0
  42. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/routing.py +0 -0
  43. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/store.py +0 -0
  44. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/updater.py +0 -0
  45. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex/usage.py +0 -0
  46. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/dependency_links.txt +0 -0
  47. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/entry_points.txt +0 -0
  48. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/requires.txt +0 -0
  49. {openreflex-0.9.2 → openreflex-0.9.4}/src/openreflex.egg-info/top_level.txt +0 -0
  50. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_completion_recap_semantics.py +0 -0
  51. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_engine.py +0 -0
  52. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_execution_state.py +0 -0
  53. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_hooks.py +0 -0
  54. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_install_cli.py +0 -0
  55. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_live_eval.py +0 -0
  56. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_outcome_closure.py +0 -0
  57. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_policy_recap.py +0 -0
  58. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_primer_background.py +0 -0
  59. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_map.py +0 -0
  60. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_memory.py +0 -0
  61. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_project_scope.py +0 -0
  62. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_quality_hardening.py +0 -0
  63. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_reflex_index.py +0 -0
  64. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_release_consistency.py +0 -0
  65. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_release_pipeline.py +0 -0
  66. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_reliability_lifecycle.py +0 -0
  67. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_routing_control.py +0 -0
  68. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_stress.py +0 -0
  69. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_token_receiver.py +0 -0
  70. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_tokens_cli.py +0 -0
  71. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_update_cli.py +0 -0
  72. {openreflex-0.9.2 → openreflex-0.9.4}/tests/test_work_modes_tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openreflex
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: Ambient project muscle memory for AI coding agents: learns and reuses project Reflexes automatically
5
5
  Author: vishnu-77
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openreflex"
7
- version = "0.9.2"
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"
@@ -1,3 +1,3 @@
1
1
  """Ambient, local execution intelligence."""
2
2
 
3
- __version__ = "0.9.2"
3
+ __version__ = "0.9.4"
@@ -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
- print(f" project reflexes: {data['project_reflexes']['visible']} "
153
- f"({data['project_reflexes']['learned']} learned / {data['project_reflexes']['proven']} proven)")
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, remove_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
- reflex_count = 0
471
+ reflex_state = "cold"
472
+ reflex_support = 0
472
473
  if enabled:
473
474
  try:
474
- from .reflexes import visible_reflexes
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
- reflex_count = len(visible_reflexes(store))
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
- reflex_count = 0
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
- print(f"Memory {experiences} experiences · {reflex_count} Reflexes")
491
- print(f"Current {reflex_name or ('learning' if experiences else 'learning project')}")
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 visible_reflexes
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 learned and proven project-specific Reflex procedures.")
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 = visible_reflexes(store)
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 learning")
630
- print(" reflexes none learned yet")
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.state}")
643
+ print(f" state {display_state(reflex)}")
636
644
  print(f" evidence {reflex.success_count} successful / {reflex.verified_count} verified")
637
- if reflex.family.startswith("area:"):
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
- else:
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 remove_statusline(project):
704
- print(" statusline OpenReflex status line removed")
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 visible_reflexes
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-specific procedures OpenReflex has learned from repeated successful work.
212
- Returns: a compact list of learned/proven Reflex names, lifecycle state, support/verification counts and
213
- procedure steps. Candidate procedures from a single run are intentionally hidden until repeated evidence
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 = visible_reflexes(e.store)
220
+ reflexes = display_reflexes(e.store)
222
221
  if not reflexes:
223
- return "OPENREFLEX / PROJECT REFLEXES\n\nNo learned project Reflexes yet."
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.state} · {reflex.success_count} successful · "
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
- lines.append(" " + " -> ".join(reflex.procedure))
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.list_reflexes(states=("learned", "proven"), limit=100_000)
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": {"visible": len(reflexes), "learned": reflex_states["learned"],
168
- "proven": reflex_states["proven"]},
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, int]:
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, int]:
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 technology/file scopes beat broad semantic scopes."""
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
- project_area = family.startswith("area:")
286
- group = [
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 (project_area or item.task_mode == experience.task_mode)
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
- group.sort(key=lambda item: item.created_at)
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 project_area else experience.task_mode
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 scope memory, not an execution recipe. Replaying a BUILD
327
- # procedure into a THINK/docs task would be actively misleading; mode-specific Reflexes keep procedures.
328
- procedure=[] if project_area else _procedure(store, successes or group or [experience]),
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 'learning project procedure'}",
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openreflex
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: Ambient project muscle memory for AI coding agents: learns and reuses project Reflexes automatically
5
5
  Author: vishnu-77
6
6
  License-Expression: MIT