sibyl-memory-cli 0.3.22__tar.gz → 0.4.0__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 (33) hide show
  1. {sibyl_memory_cli-0.3.22/src/sibyl_memory_cli.egg-info → sibyl_memory_cli-0.4.0}/PKG-INFO +4 -4
  2. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/pyproject.toml +4 -4
  3. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/cli.py +17 -1
  4. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0/src/sibyl_memory_cli.egg-info}/PKG-INFO +4 -4
  5. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli.egg-info/SOURCES.txt +1 -0
  6. sibyl_memory_cli-0.4.0/src/sibyl_memory_cli.egg-info/requires.txt +9 -0
  7. sibyl_memory_cli-0.4.0/tests/test_verdict_contract_2026_08_31.py +126 -0
  8. sibyl_memory_cli-0.3.22/src/sibyl_memory_cli.egg-info/requires.txt +0 -9
  9. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/LICENSE +0 -0
  10. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/README.md +0 -0
  11. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/setup.cfg +0 -0
  12. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/__init__.py +0 -0
  13. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/_aesthetic.py +0 -0
  14. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/_banner.py +0 -0
  15. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/migrate.py +0 -0
  16. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli/setup.py +0 -0
  17. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
  18. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
  19. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
  20. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_hermes_install_args.py +0 -0
  21. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_init_perms_guard.py +0 -0
  22. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_memory_cmd_2026_06_16.py +0 -0
  23. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_memory_tenant_2026_08_12.py +0 -0
  24. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_migrate.py +0 -0
  25. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_migrate_adversarial.py +0 -0
  26. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_migrate_force_2026_06_05.py +0 -0
  27. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_orchestrator.py +0 -0
  28. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_prelaunch_fixes_2026_06_25.py +0 -0
  29. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_quality_fixes_2026_06_30.py +0 -0
  30. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_setup.py +0 -0
  31. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_status_stores_2026_06_11.py +0 -0
  32. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_superpatch_2026_07_05.py +0 -0
  33. {sibyl_memory_cli-0.3.22 → sibyl_memory_cli-0.4.0}/tests/test_wiring_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.22
3
+ Version: 0.4.0
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT
@@ -20,11 +20,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: sibyl-memory-client>=0.6.1
24
- Requires-Dist: sibyl-memory-hermes>=0.3.2
23
+ Requires-Dist: sibyl-memory-client>=0.8.0
24
+ Requires-Dist: sibyl-memory-hermes>=0.4.0
25
25
  Requires-Dist: pyyaml<7,>=6.0
26
26
  Provides-Extra: mcp
27
- Requires-Dist: sibyl-memory-mcp>=0.1.2; extra == "mcp"
27
+ Requires-Dist: sibyl-memory-mcp>=0.2.0; extra == "mcp"
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Dynamic: license-file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sibyl-memory-cli"
7
- version = "0.3.22"
7
+ version = "0.4.0"
8
8
  description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
9
9
  authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
10
10
  license = { text = "MIT" }
@@ -23,14 +23,14 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  ]
25
25
  dependencies = [
26
- "sibyl-memory-client>=0.6.1",
27
- "sibyl-memory-hermes>=0.3.2",
26
+ "sibyl-memory-client>=0.8.0",
27
+ "sibyl-memory-hermes>=0.4.0",
28
28
  "pyyaml>=6.0,<7",
29
29
  ]
30
30
 
31
31
  [project.optional-dependencies]
32
32
  mcp = [
33
- "sibyl-memory-mcp>=0.1.2",
33
+ "sibyl-memory-mcp>=0.2.0",
34
34
  ]
35
35
  dev = [
36
36
  "pytest>=7.0",
@@ -1549,6 +1549,10 @@ def cmd_memory(args: argparse.Namespace) -> int:
1549
1549
  you can inspect any split-brain store that `sibyl status` surfaces.
1550
1550
  """
1551
1551
  from sibyl_memory_client import DEFAULT_TENANT, MemoryClient
1552
+ # THE canonical cause vocabulary (stage 3, 2026-08-31). Imported from the
1553
+ # client, never re-declared here — the CLI prints causes, it does not own
1554
+ # them.
1555
+ from sibyl_memory_client.verdicts import refine_zero
1552
1556
 
1553
1557
  db_path = Path(args.db).expanduser()
1554
1558
  print()
@@ -1582,9 +1586,21 @@ def cmd_memory(args: argparse.Namespace) -> int:
1582
1586
  print(a.kv(f"{cat}/{name}", r.get("status") or "-"))
1583
1587
  return 0
1584
1588
  if op == "search":
1585
- hits = client.search(args.query, limit=args.limit)
1589
+ # THE VERDICT CONTRACT (stage 3, 2026-08-31). This used to print
1590
+ # "(no matches)" and stop — the terminal-shaped version of the defect
1591
+ # this stage closes: a zero that cannot explain itself. The SDK now
1592
+ # returns a cause on every call, and `refine_zero` pays one COUNT on the
1593
+ # zero path so a brand-new user is told their store is EMPTY rather than
1594
+ # left to conclude their query was wrong.
1595
+ hits = refine_zero(client, client.search(args.query, limit=args.limit))
1586
1596
  if not hits:
1587
1597
  print(a.dim(f"(no matches for {args.query!r})"))
1598
+ verdict = getattr(hits, "verdict", None)
1599
+ if verdict is not None:
1600
+ # Plain language, generated from the enum and never from stored
1601
+ # text, so it is always safe to print to a terminal.
1602
+ print(a.warn_line(verdict.explain()))
1603
+ print(a.dim(f"cause: {verdict.code.value}"))
1588
1604
  return 0
1589
1605
  print(a.eyebrow(f"matches ({len(hits)})"))
1590
1606
  for h in hits:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.22
3
+ Version: 0.4.0
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT
@@ -20,11 +20,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: sibyl-memory-client>=0.6.1
24
- Requires-Dist: sibyl-memory-hermes>=0.3.2
23
+ Requires-Dist: sibyl-memory-client>=0.8.0
24
+ Requires-Dist: sibyl-memory-hermes>=0.4.0
25
25
  Requires-Dist: pyyaml<7,>=6.0
26
26
  Provides-Extra: mcp
27
- Requires-Dist: sibyl-memory-mcp>=0.1.2; extra == "mcp"
27
+ Requires-Dist: sibyl-memory-mcp>=0.2.0; extra == "mcp"
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Dynamic: license-file
@@ -26,4 +26,5 @@ tests/test_quality_fixes_2026_06_30.py
26
26
  tests/test_setup.py
27
27
  tests/test_status_stores_2026_06_11.py
28
28
  tests/test_superpatch_2026_07_05.py
29
+ tests/test_verdict_contract_2026_08_31.py
29
30
  tests/test_wiring_fix.py
@@ -0,0 +1,9 @@
1
+ sibyl-memory-client>=0.8.0
2
+ sibyl-memory-hermes>=0.4.0
3
+ pyyaml<7,>=6.0
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+
8
+ [mcp]
9
+ sibyl-memory-mcp>=0.2.0
@@ -0,0 +1,126 @@
1
+ """The verdict contract in the terminal (stage 3, 2026-08-31).
2
+
3
+ `sibyl memory search` used to print "(no matches for 'x')" and stop. That is the
4
+ terminal-shaped version of the defect this stage closes: a zero that cannot
5
+ explain itself, and in the CLI's case the person reading it is a new user
6
+ deciding whether the product works. "No matches" against an EMPTY store is the
7
+ single most misleading thing this command could say, and it was what it said.
8
+
9
+ These tests pin that every zero prints a cause in plain language, that the cause
10
+ is the canonical one from `sibyl_memory_client.verdicts` rather than a
11
+ CLI-local string, and that nothing about a successful search changed.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ import inspect
17
+ from pathlib import Path
18
+
19
+ from sibyl_memory_client import MemoryClient
20
+ from sibyl_memory_client.verdicts import GateCause, VerdictCode
21
+ from sibyl_memory_cli import cli
22
+
23
+
24
+ def _store(tmp_path: Path) -> Path:
25
+ d = tmp_path / "memory.db"
26
+ c = MemoryClient.local(path=d)
27
+ c.set_entity("partner", "Blocktronics",
28
+ {"stage": "active", "note": "token forensics suite"})
29
+ c.set_entity("partner", "Reppo", {"stage": "negotiation"})
30
+ return d
31
+
32
+
33
+ def _empty_store(tmp_path: Path) -> Path:
34
+ d = tmp_path / "empty.db"
35
+ MemoryClient.local(path=d)
36
+ return d
37
+
38
+
39
+ def test_a_zero_result_prints_a_cause_not_just_no_matches(tmp_path, capsys):
40
+ d = _store(tmp_path)
41
+ rc = cli.main(["--db", str(d), "memory", "search", "qwzjvxzzyplm"])
42
+ out = capsys.readouterr().out
43
+ assert rc == 0
44
+ assert "(no matches" in out
45
+ assert f"cause: {VerdictCode.NO_MATCH.value}" in out
46
+ assert "genuine miss" in out
47
+
48
+
49
+ def test_an_empty_store_says_so_instead_of_blaming_the_query(tmp_path, capsys):
50
+ """The one that matters most to a new user: this is a fixable state, and
51
+ 'no matches' hid that behind what looks like a failed search."""
52
+ d = _empty_store(tmp_path)
53
+ rc = cli.main(["--db", str(d), "memory", "search", "anything at all"])
54
+ out = capsys.readouterr().out
55
+ assert rc == 0
56
+ assert f"cause: {VerdictCode.EMPTY_STORE.value}" in out
57
+ assert "no rows yet" in out
58
+
59
+
60
+ def test_the_cause_is_plain_language_a_human_can_act_on(tmp_path, capsys):
61
+ d = _empty_store(tmp_path)
62
+ cli.main(["--db", str(d), "memory", "search", "anything at all"])
63
+ out = capsys.readouterr().out
64
+ # a sentence, not an enum dump
65
+ assert "Nothing was returned" in out
66
+
67
+
68
+ def test_a_successful_search_is_unchanged(tmp_path, capsys):
69
+ d = _store(tmp_path)
70
+ rc = cli.main(["--db", str(d), "memory", "search", "forensics"])
71
+ out = capsys.readouterr().out
72
+ assert rc == 0 and "Blocktronics" in out
73
+ assert "cause:" not in out # no noise on the success path
74
+
75
+ # --------------------------------------------------------------------------
76
+ # one vocabulary
77
+ # --------------------------------------------------------------------------
78
+ def _code_only(text: str) -> str:
79
+ """Strip docstrings and comments, leaving code.
80
+
81
+ Parsed, not split on `\"\"\"`. The first version of this helper did
82
+ `text.replace('\"\"\"', chr(0)).split(chr(0))[0::2]`, which silently INVERTS
83
+ which half it scans when the marker count is odd — the test would then check
84
+ the prose and skip the code, and still pass.
85
+ """
86
+ try:
87
+ tree = ast.parse(text)
88
+ except SyntaxError: # pragma: no cover
89
+ return text
90
+ spans = []
91
+ for node in ast.walk(tree):
92
+ body = getattr(node, "body", None)
93
+ if not isinstance(body, list): # ast.IfExp.body is an expr
94
+ continue
95
+ for child in body:
96
+ if (isinstance(child, ast.Expr)
97
+ and isinstance(child.value, ast.Constant)
98
+ and isinstance(child.value.value, str)):
99
+ spans.append((child.lineno, child.end_lineno))
100
+ drop = {i for a, b in spans for i in range(a, b + 1)}
101
+ return "\n".join(ln.split("#", 1)[0]
102
+ for i, ln in enumerate(text.splitlines(), 1) if i not in drop)
103
+
104
+
105
+ #: Derived from the enums, so renaming a cause updates the guard automatically.
106
+ CAUSE_LITERALS = tuple([c.value for c in VerdictCode if c is not VerdictCode.OK]
107
+ + [g.value for g in GateCause])
108
+
109
+
110
+ def _assert_declares_no_vocabulary(*modules):
111
+ """BOTH quote styles. These tests originally checked only double quotes, and
112
+ an adversarial review slipped `_LOCAL_CAUSE_COPY = 'abstained_on'` into the
113
+ MCP server past a green suite — exactly the drift the guard exists to stop."""
114
+ offenders = []
115
+ for mod in modules:
116
+ code = _code_only(inspect.getsource(mod))
117
+ for lit in CAUSE_LITERALS:
118
+ if f'"{lit}"' in code or f"'{lit}'" in code:
119
+ offenders.append((mod.__name__, lit))
120
+ assert offenders == [], f"cause strings re-declared outside verdicts.py: {offenders}"
121
+
122
+
123
+ def test_the_cli_imports_the_vocabulary_and_declares_none():
124
+ """One vocabulary, one module. The CLI prints causes; it does not own them."""
125
+ assert "from sibyl_memory_client.verdicts import refine_zero" in inspect.getsource(cli)
126
+ _assert_declares_no_vocabulary(cli)
@@ -1,9 +0,0 @@
1
- sibyl-memory-client>=0.6.1
2
- sibyl-memory-hermes>=0.3.2
3
- pyyaml<7,>=6.0
4
-
5
- [dev]
6
- pytest>=7.0
7
-
8
- [mcp]
9
- sibyl-memory-mcp>=0.1.2