claude-smart 0.2.37 → 0.2.40

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.
@@ -14,9 +14,8 @@ Exposes the following subcommands:
14
14
  - ``learn``: publish unpublished interactions and force reflexio
15
15
  extraction now over the active session buffer.
16
16
  - ``restart``: stop and restart the reflexio backend + dashboard services
17
- (rebuilding the dashboard bundle) so local edits under the ``reflexio``
18
- submodule or ``plugin/dashboard/`` take effect without restarting Claude
19
- Code.
17
+ (rebuilding the dashboard bundle) so local Reflexio checkout edits or
18
+ ``plugin/dashboard/`` edits take effect without restarting Claude Code.
20
19
  """
21
20
 
22
21
  from __future__ import annotations
@@ -30,6 +29,7 @@ import shutil
30
29
  import subprocess
31
30
  import sys
32
31
  import time
32
+ from collections.abc import Iterable
33
33
  from dataclasses import dataclass
34
34
  from pathlib import Path
35
35
 
@@ -82,6 +82,7 @@ _LOCAL_DATA_NOTICE = (
82
82
  " rm -rf ~/.claude-smart ~/.reflexio\n"
83
83
  )
84
84
  _INSTALL_FAILURE_MARKER = _STATE_DIR / "install-failed"
85
+ _SERVICE_STATUS_PROBE_FAILED = "probe_failed"
85
86
  _DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
86
87
  _REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json"
87
88
  _LOCAL_STORAGE_ENV = "LOCAL_STORAGE_PATH"
@@ -108,6 +109,94 @@ _COPYTREE_IGNORE = shutil.ignore_patterns(
108
109
  )
109
110
 
110
111
 
112
+ def _windows_path_text(path: str) -> str:
113
+ """Normalize slashes/case for Windows path checks."""
114
+ return path.replace("/", "\\").lower()
115
+
116
+
117
+ _WINDOWS_SYSTEM_BASH_SUFFIXES = (
118
+ "\\windows\\system32\\bash.exe",
119
+ "\\windows\\sysnative\\bash.exe",
120
+ "\\windows\\syswow64\\bash.exe",
121
+ )
122
+
123
+
124
+ def _is_windows_system_bash(path: str) -> bool:
125
+ normalized = _windows_path_text(path)
126
+ return normalized.endswith(_WINDOWS_SYSTEM_BASH_SUFFIXES)
127
+
128
+
129
+ def _resolve_candidate(candidate: str) -> str | None:
130
+ expanded = os.path.expandvars(os.path.expanduser(candidate))
131
+ if os.path.isfile(expanded):
132
+ return expanded
133
+ return shutil.which(expanded)
134
+
135
+
136
+ def _iter_path_candidates(names: tuple[str, ...]) -> list[str]:
137
+ found: list[str] = []
138
+ for raw_dir in os.environ.get("PATH", "").split(os.pathsep):
139
+ if not raw_dir:
140
+ continue
141
+ directory = os.path.expandvars(os.path.expanduser(raw_dir))
142
+ for name in names:
143
+ candidate = os.path.join(directory, name)
144
+ if os.path.isfile(candidate):
145
+ found.append(candidate)
146
+ return found
147
+
148
+
149
+ def _first_non_system_bash(candidates: Iterable[str]) -> str | None:
150
+ for candidate in candidates:
151
+ resolved = _resolve_candidate(candidate)
152
+ if resolved and not _is_windows_system_bash(resolved):
153
+ return resolved
154
+ return None
155
+
156
+
157
+ def _resolve_bash() -> str | None:
158
+ """Resolve a bash executable suitable for running bundled shell scripts.
159
+
160
+ Returns:
161
+ Absolute path to a usable bash, or None when none is available.
162
+ On Windows, the WSL stub under ``\\Windows\\System32`` (and the
163
+ ``Sysnative`` / ``SysWOW64`` redirectors) is rejected so that bundled
164
+ ``*.sh`` scripts run under Git-Bash instead.
165
+ """
166
+ if os.name != "nt":
167
+ return shutil.which("bash")
168
+
169
+ sources: list[Iterable[str]] = []
170
+ bash_env = os.environ.get("BASH", "").strip()
171
+ if bash_env:
172
+ sources.append((bash_env,))
173
+ sources.append(
174
+ (
175
+ r"C:\Program Files\Git\bin\bash.exe",
176
+ r"C:\Program Files (x86)\Git\bin\bash.exe",
177
+ )
178
+ )
179
+ sources.append(_iter_path_candidates(("bash.exe", "bash")))
180
+ sources.append(("bash.exe", "bash"))
181
+
182
+ for source in sources:
183
+ resolved = _first_non_system_bash(source)
184
+ if resolved:
185
+ return resolved
186
+ return None
187
+
188
+
189
+ def _resolve_npm() -> str | None:
190
+ """Resolve npm, accounting for Windows .cmd/.exe shims."""
191
+ if os.name == "nt":
192
+ for candidate in ("npm.cmd", "npm.exe", "npm"):
193
+ resolved = shutil.which(candidate)
194
+ if resolved:
195
+ return resolved
196
+ return None
197
+ return shutil.which("npm")
198
+
199
+
111
200
  def _latest_session_id() -> str | None:
112
201
  """Most-recently-modified session JSONL in the state dir. None if none exist."""
113
202
  root = state.state_dir()
@@ -201,7 +290,7 @@ def _bootstrap_claude_code_install() -> tuple[bool, str]:
201
290
  _force_plugin_root(plugin_root)
202
291
  except OSError as exc:
203
292
  return False, str(exc)
204
- bash = shutil.which("bash")
293
+ bash = _resolve_bash()
205
294
  if not bash:
206
295
  return False, "bash is required to bootstrap claude-smart dependencies"
207
296
  result = subprocess.run(
@@ -805,7 +894,7 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
805
894
 
806
895
 
807
896
  def _configure_reflexio_setup() -> bool:
808
- """Load setup state from ``~/.reflexio/.env`` without writing local defaults.
897
+ """Load setup state and ensure local defaults when unmanaged.
809
898
 
810
899
  Returns:
811
900
  bool: Whether read-only mode is enabled.
@@ -850,7 +939,11 @@ def _configure_reflexio_setup() -> bool:
850
939
  else:
851
940
  os.environ.pop(env_config.REFLEXIO_URL_ENV, None)
852
941
  os.environ.pop(env_config.REFLEXIO_API_KEY_ENV, None)
942
+ os.environ.pop("REFLEXIO_USER_ID", None)
853
943
  os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None)
944
+ added = env_config.ensure_local_env_defaults(_REFLEXIO_ENV_PATH)
945
+ if added:
946
+ sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
854
947
  return read_only
855
948
 
856
949
 
@@ -1251,8 +1344,14 @@ def _run_service(script: Path, subcmd: str) -> int:
1251
1344
  if not script.exists():
1252
1345
  sys.stderr.write(f"error: {script} not found\n")
1253
1346
  return 1
1347
+ bash = _resolve_bash()
1348
+ if not bash:
1349
+ sys.stderr.write(
1350
+ "error: bash is required to run claude-smart service scripts\n"
1351
+ )
1352
+ return 1
1254
1353
  try:
1255
- subprocess.run([str(script), subcmd], check=True)
1354
+ subprocess.run([bash, str(script), subcmd], check=True)
1256
1355
  return 0
1257
1356
  except subprocess.CalledProcessError as exc:
1258
1357
  return exc.returncode or 1
@@ -1277,10 +1376,16 @@ def _service_status(script: Path, wait_ready_s: float = 3.0) -> str:
1277
1376
  """
1278
1377
  if not script.exists():
1279
1378
  return "script missing"
1379
+ bash = _resolve_bash()
1380
+ if not bash:
1381
+ return _SERVICE_STATUS_PROBE_FAILED
1280
1382
  deadline = time.monotonic() + wait_ready_s
1281
1383
  while True:
1282
1384
  result = subprocess.run(
1283
- [str(script), "status"], capture_output=True, text=True, check=False
1385
+ [bash, str(script), "status"],
1386
+ capture_output=True,
1387
+ text=True,
1388
+ check=False,
1284
1389
  )
1285
1390
  status = result.stdout.strip() or "unknown"
1286
1391
  if status != "not running" or time.monotonic() >= deadline:
@@ -1305,6 +1410,10 @@ def _is_running_status(status: str) -> bool:
1305
1410
  return status.startswith("running on ")
1306
1411
 
1307
1412
 
1413
+ def _is_confirmed_stopped_status(status: str) -> bool:
1414
+ return status == "not running"
1415
+
1416
+
1308
1417
  def _read_dotenv_value(env_path: Path, key: str) -> str | None:
1309
1418
  """Read one simple KEY=VALUE binding from a dotenv file."""
1310
1419
  if not env_path.is_file():
@@ -1507,7 +1616,7 @@ def cmd_restart(args: argparse.Namespace) -> int:
1507
1616
  Stops both long-lived services, optionally rebuilds the dashboard's
1508
1617
  Next.js bundle so source edits under ``plugin/dashboard/`` take effect,
1509
1618
  then starts them again. Useful during local development when iterating
1510
- on the ``reflexio`` submodule or the dashboard.
1619
+ on the local Reflexio checkout or the dashboard.
1511
1620
 
1512
1621
  Args:
1513
1622
  args (argparse.Namespace): Parsed CLI args. Honors ``args.skip_backend``,
@@ -1536,15 +1645,15 @@ def cmd_restart(args: argparse.Namespace) -> int:
1536
1645
  sys.stderr.write(
1537
1646
  f"warning: dashboard dir {_DASHBOARD_DIR} missing; skipping rebuild\n"
1538
1647
  )
1539
- elif not shutil.which("npm"):
1648
+ elif not (npm := _resolve_npm()):
1540
1649
  sys.stderr.write("warning: npm not on PATH; serving previous build\n")
1541
1650
  else:
1542
1651
  next_bin = _DASHBOARD_DIR / "node_modules" / ".bin" / "next"
1543
1652
  if not next_bin.exists():
1544
1653
  install_cmd = (
1545
- ["npm", "ci", "--no-audit", "--no-fund"]
1654
+ [npm, "ci", "--no-audit", "--no-fund"]
1546
1655
  if (_DASHBOARD_DIR / "package-lock.json").exists()
1547
- else ["npm", "install", "--no-audit", "--no-fund"]
1656
+ else [npm, "install", "--no-audit", "--no-fund"]
1548
1657
  )
1549
1658
  install_label = " ".join(install_cmd)
1550
1659
  sys.stdout.write(
@@ -1573,10 +1682,11 @@ def cmd_restart(args: argparse.Namespace) -> int:
1573
1682
  "Rebuilding dashboard (npm run build, may take a minute)…\n"
1574
1683
  )
1575
1684
  try:
1576
- subprocess.run(["npm", "run", "build"], cwd=_DASHBOARD_DIR, check=True)
1577
- except subprocess.CalledProcessError as exc:
1685
+ subprocess.run([npm, "run", "build"], cwd=_DASHBOARD_DIR, check=True)
1686
+ except (subprocess.CalledProcessError, OSError) as exc:
1687
+ returncode = getattr(exc, "returncode", 1) or 1
1578
1688
  sys.stderr.write(
1579
- f"error: dashboard build failed (exit {exc.returncode}); "
1689
+ f"error: dashboard build failed (exit {returncode}); "
1580
1690
  "not starting dashboard.\n"
1581
1691
  )
1582
1692
  if do_backend:
@@ -1585,7 +1695,7 @@ def cmd_restart(args: argparse.Namespace) -> int:
1585
1695
  sys.stdout.write(
1586
1696
  f"reflexio backend: {_service_status(_BACKEND_SCRIPT)}\n"
1587
1697
  )
1588
- return exc.returncode or 1
1698
+ return returncode
1589
1699
 
1590
1700
  start_rc = 0
1591
1701
  if do_backend:
@@ -1643,7 +1753,14 @@ def cmd_clear_all(args: argparse.Namespace) -> int:
1643
1753
  )
1644
1754
  return 1
1645
1755
 
1646
- was_running = _is_running_status(_service_status(_BACKEND_SCRIPT, wait_ready_s=0.0))
1756
+ initial_status = _service_status(_BACKEND_SCRIPT, wait_ready_s=0.0)
1757
+ was_running = _is_running_status(initial_status)
1758
+ if not was_running and not _is_confirmed_stopped_status(initial_status):
1759
+ sys.stderr.write(
1760
+ "error: could not confirm reflexio backend is stopped "
1761
+ f"({initial_status}); aborting without deleting data\n"
1762
+ )
1763
+ return 1
1647
1764
  if was_running:
1648
1765
  sys.stdout.write("Stopping reflexio backend…\n")
1649
1766
  stop_rc = _run_service(_BACKEND_SCRIPT, "stop")
@@ -1659,6 +1776,12 @@ def cmd_clear_all(args: argparse.Namespace) -> int:
1659
1776
  "aborting without deleting data\n"
1660
1777
  )
1661
1778
  return 1
1779
+ if not _is_confirmed_stopped_status(stopped_status):
1780
+ sys.stderr.write(
1781
+ "error: could not confirm reflexio backend stopped "
1782
+ f"({stopped_status}); aborting without deleting data\n"
1783
+ )
1784
+ return 1
1662
1785
 
1663
1786
  removed_targets = 0
1664
1787
  try:
@@ -238,6 +238,11 @@ def render_inline_compact_with_registry(
238
238
  item = f"{content} (title: {title}"
239
239
  if rule_url:
240
240
  item += f"; open: {rule_url}"
241
+ marker_parts.append(
242
+ _markdown_link(
243
+ rule_url, _strip_trailing_sentence_punctuation(title)
244
+ )
245
+ )
241
246
  item += ")"
242
247
  if entry.get("kind") == "profile":
243
248
  preference_parts.append(item)
@@ -280,6 +285,17 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
280
285
  "the same linked memory text; keep the link, but do not show the "
281
286
  "URL. Skip when unrelated."
282
287
  )
288
+ if marker_parts:
289
+ marker = f"✨ claude-smart rule applied: {' | '.join(marker_parts)}"
290
+ separator_instruction = (
291
+ " Separate multiple linked memories with the visible ` | ` separator."
292
+ if len(marker_parts) > 1
293
+ else ""
294
+ )
295
+ return _remoteize_citation_instruction(
296
+ f"If used, copy this final marker exactly with markdown links: "
297
+ f"`{marker}`.{separator_instruction} Skip when unrelated."
298
+ )
283
299
  return _remoteize_citation_instruction(
284
300
  "Only if a listed [cs:...] item materially changes your answer, end "
285
301
  "with one final marker like `✨ claude-smart rule applied: "
@@ -368,7 +384,7 @@ def _append_playbook_bullet(
368
384
  item_id = cs_cite.rank_id("playbook", rank, real_id)
369
385
  title = _title_from_content(content)
370
386
  dashboard_url = _dashboard_url("playbook", real_id, source_kind)
371
- rule_url = _rule_url(item_id, "playbook")
387
+ rule_url = _rule_url(item_id, "playbook", real_id, source_kind)
372
388
  bullet = f"- [cs:{item_id}] {content}"
373
389
  if trigger:
374
390
  bullet += f" _(when: {trigger})_"
@@ -407,7 +423,7 @@ def _format_profiles(
407
423
  item_id = cs_cite.rank_id("profile", rank, real_id)
408
424
  title = _title_from_content(content)
409
425
  dashboard_url = _dashboard_url("profile", real_id)
410
- rule_url = _rule_url(item_id, "profile")
426
+ rule_url = _rule_url(item_id, "profile", real_id)
411
427
  bullet = f"- [cs:{item_id}] {content}"
412
428
  if rule_url:
413
429
  bullet += f" _(open: {rule_url})_"
@@ -427,7 +443,7 @@ def _format_profiles(
427
443
 
428
444
 
429
445
  def _dashboard_url(kind: str, real_id: Any, source_kind: str | None = None) -> str:
430
- remote_url = _remote_reflexio_page_url(kind)
446
+ remote_url = _remote_reflexio_item_url(kind, real_id, source_kind)
431
447
  if remote_url:
432
448
  return remote_url
433
449
  if real_id is None:
@@ -442,8 +458,10 @@ def _dashboard_url(kind: str, real_id: Any, source_kind: str | None = None) -> s
442
458
  return ""
443
459
 
444
460
 
445
- def _rule_url(item_id: str, kind: str) -> str:
446
- remote_url = _remote_reflexio_page_url(kind)
461
+ def _rule_url(
462
+ item_id: str, kind: str, real_id: Any = None, source_kind: str | None = None
463
+ ) -> str:
464
+ remote_url = _remote_reflexio_item_url(kind, real_id, source_kind)
447
465
  if remote_url:
448
466
  return remote_url
449
467
  if not item_id:
@@ -453,6 +471,27 @@ def _rule_url(item_id: str, kind: str) -> str:
453
471
  return f"{base}/rules/{encoded_id}"
454
472
 
455
473
 
474
+ def _remote_reflexio_item_url(
475
+ kind: str, real_id: Any, source_kind: str | None = None
476
+ ) -> str:
477
+ origin = _remote_reflexio_origin()
478
+ if not origin:
479
+ return ""
480
+ if real_id is None:
481
+ return _remote_reflexio_page_url(kind)
482
+ encoded_id = quote(str(real_id), safe="")
483
+ if kind == "profile":
484
+ return f"{origin}/profiles?profile_id={encoded_id}"
485
+ if kind == "playbook":
486
+ if source_kind == "user_playbook":
487
+ return (
488
+ f"{origin}/playbooks?resource=user_playbook&"
489
+ f"user_playbook_id={encoded_id}"
490
+ )
491
+ return f"{origin}/playbooks?agent_playbook_id={encoded_id}"
492
+ return ""
493
+
494
+
456
495
  def _remote_reflexio_page_url(kind: str) -> str:
457
496
  origin = _remote_reflexio_origin()
458
497
  if not origin:
@@ -504,6 +543,12 @@ def _osc8_link(url: str, label: str) -> str:
504
543
  return f"\x1b]8;;{url}\x1b\\{label}\x1b]8;;\x1b\\"
505
544
 
506
545
 
546
+ def _markdown_link(url: str, label: str) -> str:
547
+ safe_label = label.replace("[", "\\[").replace("]", "\\]")
548
+ safe_url = url.replace(")", "%29")
549
+ return f"[{safe_label}]({safe_url})"
550
+
551
+
507
552
  def _strip_trailing_sentence_punctuation(text: str) -> str:
508
553
  return text.rstrip().rstrip(".!?")
509
554
 
@@ -39,7 +39,7 @@ from __future__ import annotations
39
39
 
40
40
  import re
41
41
  from typing import Any
42
- from urllib.parse import unquote, urlparse
42
+ from urllib.parse import parse_qs, unquote, urlparse
43
43
 
44
44
  _FINGERPRINT_LEN = 4
45
45
 
@@ -287,6 +287,17 @@ def dashboard_url_token(url: str) -> str:
287
287
  parsed = urlparse(url)
288
288
  path = parsed.path if parsed.scheme else url.split("?", 1)[0].split("#", 1)[0]
289
289
  parts = [unquote(part) for part in path.strip("/").split("/") if part]
290
+ query = parse_qs(parsed.query)
291
+ if len(parts) == 1 and parts[0] == "profiles":
292
+ profile_ids = query.get("profile_id") or []
293
+ return f"route:profile:profile:{profile_ids[0]}" if profile_ids else ""
294
+ if len(parts) == 1 and parts[0] == "playbooks":
295
+ user_playbook_ids = query.get("user_playbook_id") or []
296
+ if query.get("resource") == ["user_playbook"] and user_playbook_ids:
297
+ return f"route:playbook:user_playbook:{user_playbook_ids[0]}"
298
+ agent_playbook_ids = query.get("agent_playbook_id") or []
299
+ if agent_playbook_ids:
300
+ return f"route:playbook:agent_playbook:{agent_playbook_ids[0]}"
290
301
  if len(parts) == 3 and parts[0] == "skills" and parts[1] in {"project", "shared"}:
291
302
  source_kind = "user_playbook" if parts[1] == "project" else "agent_playbook"
292
303
  return f"route:playbook:{source_kind}:{parts[2]}"
@@ -10,6 +10,27 @@ MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/"
10
10
  REFLEXIO_URL_ENV = "REFLEXIO_URL"
11
11
  REFLEXIO_API_KEY_ENV = "REFLEXIO_API_KEY"
12
12
  CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY"
13
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI"
14
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING"
15
+
16
+ _LOCAL_DEFAULT_ENTRIES = (
17
+ (
18
+ "# Route reflexio generation through the local Claude Code CLI",
19
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
20
+ "1",
21
+ ),
22
+ (
23
+ "# Use the in-process ONNX embedder (chromadb) - no API key for semantic search",
24
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
25
+ "1",
26
+ ),
27
+ (None, CLAUDE_SMART_READ_ONLY_ENV, "0"),
28
+ )
29
+ _LOCAL_MODE_PRUNE_KEYS = {
30
+ REFLEXIO_URL_ENV,
31
+ REFLEXIO_API_KEY_ENV,
32
+ "REFLEXIO_USER_ID",
33
+ }
13
34
 
14
35
 
15
36
  def parse_env_line(line: str) -> tuple[str, str] | None:
@@ -89,6 +110,59 @@ def set_env_vars(path: Path, values: dict[str, str]) -> list[str]:
89
110
  return added
90
111
 
91
112
 
113
+ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
114
+ """Create or augment ``~/.reflexio/.env`` for claude-smart local mode.
115
+
116
+ Existing active assignments win. This repairs first installs and deleted
117
+ env files without clobbering explicit user overrides such as
118
+ ``CLAUDE_SMART_READ_ONLY=1``.
119
+ """
120
+ path = path or REFLEXIO_ENV_PATH
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ try:
123
+ existing = path.read_text()
124
+ except OSError:
125
+ existing = ""
126
+
127
+ present: set[str] = set()
128
+ kept_lines: list[str] = []
129
+ pruned = False
130
+ for line in existing.splitlines():
131
+ parsed = parse_env_line(line)
132
+ if parsed is not None:
133
+ key, _value = parsed
134
+ if key in _LOCAL_MODE_PRUNE_KEYS:
135
+ pruned = True
136
+ continue
137
+ present.add(key)
138
+ kept_lines.append(line)
139
+
140
+ additions: list[str] = []
141
+ added_keys: list[str] = []
142
+ for comment, key, value in _LOCAL_DEFAULT_ENTRIES:
143
+ if key in present:
144
+ continue
145
+ if comment:
146
+ additions.append(comment)
147
+ if key == CLAUDE_SMART_READ_ONLY_ENV:
148
+ additions.append(f'{key}="{_escape_env_value(value)}"')
149
+ else:
150
+ additions.append(f"{key}={_escape_env_value(value)}")
151
+ added_keys.append(key)
152
+
153
+ if additions or pruned:
154
+ content = "\n".join(kept_lines)
155
+ if additions:
156
+ prefix = "" if not content or content.endswith("\n") else "\n"
157
+ content = content + prefix + "\n".join(additions)
158
+ content = content + ("\n" if content else "")
159
+ path.write_text(content, encoding="utf-8")
160
+ elif not path.exists():
161
+ path.touch()
162
+ path.chmod(0o600)
163
+ return added_keys
164
+
165
+
92
166
  def env_truthy(name: str) -> bool:
93
167
  """Return True when an environment flag is explicitly enabled."""
94
168
  return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
@@ -79,7 +79,7 @@ def log_event(
79
79
  fires against workspace directories.
80
80
  internal_skipped (bool): True when ``is_internal_invocation``
81
81
  short-circuited the dispatcher (reflexio-internal sub-claude
82
- calls, or invocations from inside the reflexio submodule).
82
+ calls, or invocations from inside a Reflexio checkout).
83
83
  handler_status (str): ``"ok"`` for a clean return, ``"unknown_event"``
84
84
  when no handler is registered, or ``"raised:<ExcClass>: <msg>"``
85
85
  when the handler raised — formatted by ``hook.main``.
@@ -26,9 +26,9 @@ Detection signals, OR'd:
26
26
  - Env var ``CLAUDE_SMART_INTERNAL=1``, set by reflexio's provider
27
27
  before spawning ``claude``. Belt-and-suspenders for case (1) in
28
28
  case the entrypoint check ever misses a future SDK variant.
29
- - ``payload.cwd`` resolves inside the reflexio submodule. Catches
30
- direct interactive ``claude`` runs from inside reflexio (manual
31
- debugging) that would otherwise pollute the corpus.
29
+ - ``payload.cwd`` resolves inside the side-by-side reflexio checkout. Catches
30
+ direct interactive ``claude`` runs from inside reflexio during local
31
+ debugging that would otherwise pollute the corpus.
32
32
  - Known Codex-internal prompt templates (title generation and home-screen
33
33
  suggestions). These are model calls made by Codex itself, not user
34
34
  coding turns, and must never be reflected into claude-smart memory.
@@ -61,11 +61,11 @@ _CODEX_SUGGESTIONS_APPS_MARKER = (
61
61
  "their connected apps."
62
62
  )
63
63
 
64
- # Reflexio submodule lives at <repo>/reflexio when this package runs from
65
- # a dev checkout (<repo>/plugin/src/claude_smart/internal_call.py); anchor
66
- # relative to this file so the check follows the real checkout if the
67
- # repo is relocated. In install mode the submodule is absent — the env
68
- # marker is the primary signal and this path never matches.
64
+ # Reflexio usually lives next to claude-smart during local development:
65
+ # <workspace>/claude-smart and <workspace>/reflexio. Anchor relative to this
66
+ # file so the check follows the real checkout if the repo is relocated. In
67
+ # install mode that sibling checkout is absent — the env marker is the primary
68
+ # signal and this path never matches.
69
69
  #
70
70
  # The path computation is tightly coupled to the current layout: if this
71
71
  # module moves, ``_REFLEXIO_DIR`` silently stops matching and only the
@@ -73,8 +73,8 @@ _CODEX_SUGGESTIONS_APPS_MARKER = (
73
73
  # tests) override the path without touching the module.
74
74
  _THIS_DIR = Path(__file__).resolve().parent
75
75
  _REFLEXIO_DIR = Path(
76
- os.environ.get("CLAUDE_SMART_REFLEXIO_DIR") or _THIS_DIR.parents[2] / "reflexio"
77
- )
76
+ os.environ.get("CLAUDE_SMART_REFLEXIO_DIR") or _THIS_DIR.parents[3] / "reflexio"
77
+ ).resolve()
78
78
 
79
79
 
80
80
  def is_internal_invocation(payload: dict[str, Any]) -> bool:
@@ -86,7 +86,7 @@ def is_internal_invocation(payload: dict[str, Any]) -> bool:
86
86
 
87
87
  Returns:
88
88
  bool: True when the env marker is set or ``cwd`` points inside
89
- the reflexio submodule. False otherwise, including when
89
+ the Reflexio checkout. False otherwise, including when
90
90
  ``cwd`` is missing or unresolvable.
91
91
  """
92
92
  if runtime.is_internal_invocation_env():