agmem 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (45) hide show
  1. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/METADATA +138 -14
  2. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/RECORD +45 -26
  3. memvcs/cli.py +10 -0
  4. memvcs/commands/add.py +6 -0
  5. memvcs/commands/audit.py +59 -0
  6. memvcs/commands/clone.py +7 -0
  7. memvcs/commands/daemon.py +28 -0
  8. memvcs/commands/distill.py +16 -0
  9. memvcs/commands/federated.py +53 -0
  10. memvcs/commands/fsck.py +31 -0
  11. memvcs/commands/garden.py +14 -0
  12. memvcs/commands/gc.py +51 -0
  13. memvcs/commands/merge.py +55 -1
  14. memvcs/commands/prove.py +66 -0
  15. memvcs/commands/pull.py +27 -0
  16. memvcs/commands/resolve.py +130 -0
  17. memvcs/commands/verify.py +74 -23
  18. memvcs/core/audit.py +124 -0
  19. memvcs/core/consistency.py +9 -9
  20. memvcs/core/crypto_verify.py +280 -0
  21. memvcs/core/distiller.py +25 -25
  22. memvcs/core/encryption.py +169 -0
  23. memvcs/core/federated.py +86 -0
  24. memvcs/core/gardener.py +23 -24
  25. memvcs/core/ipfs_remote.py +39 -0
  26. memvcs/core/knowledge_graph.py +1 -0
  27. memvcs/core/llm/__init__.py +10 -0
  28. memvcs/core/llm/anthropic_provider.py +50 -0
  29. memvcs/core/llm/base.py +27 -0
  30. memvcs/core/llm/factory.py +30 -0
  31. memvcs/core/llm/openai_provider.py +36 -0
  32. memvcs/core/merge.py +36 -23
  33. memvcs/core/objects.py +16 -6
  34. memvcs/core/pack.py +92 -0
  35. memvcs/core/privacy_budget.py +63 -0
  36. memvcs/core/remote.py +38 -0
  37. memvcs/core/repository.py +82 -2
  38. memvcs/core/temporal_index.py +9 -0
  39. memvcs/core/trust.py +103 -0
  40. memvcs/core/vector_store.py +15 -1
  41. memvcs/core/zk_proofs.py +26 -0
  42. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  43. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  44. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  45. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/commands/fsck.py CHANGED
@@ -66,6 +66,10 @@ class FsckCommand:
66
66
  issues_found += ref_issues
67
67
  issues_fixed += ref_fixed
68
68
 
69
+ # Cryptographic verification (Merkle + signature)
70
+ crypto_issues = FsckCommand._check_crypto(repo, args.verbose)
71
+ issues_found += crypto_issues
72
+
69
73
  # Print summary
70
74
  print()
71
75
  print("=" * 40)
@@ -195,3 +199,30 @@ class FsckCommand:
195
199
  print(f" Found {issues} ref issues")
196
200
 
197
201
  return issues, 0
202
+
203
+ @staticmethod
204
+ def _check_crypto(repo, verbose: bool) -> int:
205
+ """Verify Merkle/signature on branch tips. Returns number of issues."""
206
+ print("\nChecking commit signatures...")
207
+ try:
208
+ from ..core.crypto_verify import verify_commit, load_public_key
209
+ except ImportError:
210
+ if verbose:
211
+ print(" Crypto verification not available")
212
+ return 0
213
+ issues = 0
214
+ pub = load_public_key(repo.mem_dir)
215
+ for branch in repo.refs.list_branches():
216
+ ch = repo.refs.get_branch_commit(branch)
217
+ if not ch:
218
+ continue
219
+ ok, err = verify_commit(repo.object_store, ch, public_key_pem=pub, mem_dir=repo.mem_dir)
220
+ if not ok:
221
+ issues += 1
222
+ if verbose:
223
+ print(f" {branch} ({ch[:8]}): {err}")
224
+ if issues == 0:
225
+ print(" Commit signatures consistent")
226
+ else:
227
+ print(f" Found {issues} commit(s) with verification issues")
228
+ return issues
memvcs/commands/garden.py CHANGED
@@ -38,6 +38,11 @@ class GardenCommand:
38
38
  help="LLM provider for summarization (default: none)",
39
39
  )
40
40
  parser.add_argument("--model", help="LLM model to use (e.g., gpt-3.5-turbo)")
41
+ parser.add_argument(
42
+ "--private",
43
+ action="store_true",
44
+ help="Use differential privacy (spend epsilon from budget)",
45
+ )
41
46
 
42
47
  @staticmethod
43
48
  def execute(args) -> int:
@@ -45,6 +50,15 @@ class GardenCommand:
45
50
  if code != 0:
46
51
  return code
47
52
 
53
+ if getattr(args, "private", False):
54
+ from ..core.privacy_budget import load_budget, spend_epsilon
55
+
56
+ spent, max_eps, _ = load_budget(repo.mem_dir)
57
+ epsilon_cost = 0.1
58
+ if not spend_epsilon(repo.mem_dir, epsilon_cost):
59
+ print(f"Privacy budget exceeded (spent {spent:.2f}, max {max_eps}).")
60
+ return 1
61
+
48
62
  # Build config
49
63
  config = GardenerConfig(
50
64
  threshold=args.threshold,
memvcs/commands/gc.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ agmem gc - Garbage collection.
3
+
4
+ Remove unreachable objects; optionally repack.
5
+ """
6
+
7
+ import argparse
8
+
9
+ from ..commands.base import require_repo
10
+ from ..core.pack import run_gc
11
+
12
+
13
+ class GcCommand:
14
+ """Garbage collect unreachable objects."""
15
+
16
+ name = "gc"
17
+ help = "Remove unreachable objects (garbage collection)"
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ "--dry-run",
23
+ action="store_true",
24
+ help="Report what would be removed without deleting",
25
+ )
26
+ parser.add_argument(
27
+ "--prune-days",
28
+ type=int,
29
+ default=90,
30
+ metavar="N",
31
+ help="Consider reflog entries within N days (default 90)",
32
+ )
33
+
34
+ @staticmethod
35
+ def execute(args) -> int:
36
+ repo, code = require_repo()
37
+ if code != 0:
38
+ return code
39
+
40
+ gc_prune_days = getattr(args, "prune_days", 90)
41
+ deleted, freed = run_gc(
42
+ repo.mem_dir,
43
+ repo.object_store,
44
+ gc_prune_days=gc_prune_days,
45
+ dry_run=args.dry_run,
46
+ )
47
+ if args.dry_run:
48
+ print(f"Would remove {deleted} unreachable object(s) ({freed} bytes).")
49
+ else:
50
+ print(f"Removed {deleted} unreachable object(s) ({freed} bytes reclaimed).")
51
+ return 0
memvcs/commands/merge.py CHANGED
@@ -23,6 +23,11 @@ class MergeCommand:
23
23
  "--no-commit", action="store_true", help="Perform merge but do not commit"
24
24
  )
25
25
  parser.add_argument("--abort", action="store_true", help="Abort the current merge")
26
+ parser.add_argument(
27
+ "--yes",
28
+ action="store_true",
29
+ help="Accept conditionally trusted branch commits without prompting",
30
+ )
26
31
 
27
32
  @staticmethod
28
33
  def execute(args) -> int:
@@ -53,6 +58,24 @@ class MergeCommand:
53
58
  print(f"Error: Branch '{args.branch}' not found.")
54
59
  return 1
55
60
 
61
+ # Trust check for branch tip (may be from another agent)
62
+ other_commit_hash = repo.refs.get_branch_commit(args.branch)
63
+ if other_commit_hash:
64
+ from ..core.objects import Commit
65
+ from ..core.trust import find_verifying_key, get_trust_level
66
+
67
+ other_commit = Commit.load(repo.object_store, other_commit_hash)
68
+ if other_commit and other_commit.metadata:
69
+ key_pem = find_verifying_key(repo.mem_dir, other_commit.metadata)
70
+ if key_pem is not None:
71
+ level = get_trust_level(repo.mem_dir, key_pem)
72
+ if level == "untrusted":
73
+ print(f"Merge blocked: branch '{args.branch}' signed by untrusted key.")
74
+ return 1
75
+ if level == "conditional" and not getattr(args, "yes", False):
76
+ print("Branch signed by conditionally trusted key. Use --yes to merge.")
77
+ return 1
78
+
56
79
  # Perform merge
57
80
  engine = MergeEngine(repo)
58
81
  result = engine.merge(args.branch, message=args.message)
@@ -61,16 +84,47 @@ class MergeCommand:
61
84
  print(f"Merge successful: {result.message}")
62
85
  if result.commit_hash:
63
86
  print(f" Commit: {result.commit_hash[:8]}")
87
+ try:
88
+ from ..core.audit import append_audit
89
+
90
+ append_audit(
91
+ repo.mem_dir, "merge", {"branch": args.branch, "commit": result.commit_hash}
92
+ )
93
+ except Exception:
94
+ pass
64
95
  return 0
65
96
  else:
66
97
  print(f"Merge failed: {result.message}")
67
98
 
68
99
  if result.conflicts:
100
+ # Persist conflicts for agmem resolve
101
+ try:
102
+ import json
103
+
104
+ merge_dir = repo.mem_dir / "merge"
105
+ merge_dir.mkdir(parents=True, exist_ok=True)
106
+ conflicts_data = [
107
+ {
108
+ "path": c.path,
109
+ "message": c.message,
110
+ "memory_type": getattr(c, "memory_type", None),
111
+ "payload": getattr(c, "payload", None),
112
+ "ours_content": c.ours_content,
113
+ "theirs_content": c.theirs_content,
114
+ "base_content": c.base_content,
115
+ }
116
+ for c in result.conflicts
117
+ ]
118
+ (merge_dir / "conflicts.json").write_text(json.dumps(conflicts_data, indent=2))
119
+ except Exception:
120
+ pass
69
121
  print()
70
122
  print("Conflicts:")
71
123
  for conflict in result.conflicts:
72
124
  print(f" {conflict.path}")
73
125
  print()
74
- print("Resolve conflicts and run 'agmem commit' to complete the merge.")
126
+ print(
127
+ "Resolve conflicts with 'agmem resolve' or edit files and run 'agmem commit'."
128
+ )
75
129
 
76
130
  return 1
@@ -0,0 +1,66 @@
1
+ """
2
+ agmem prove - Generate zero-knowledge proofs (stub).
3
+
4
+ Prove properties of memory (keyword, freshness) without revealing content.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.zk_proofs import prove_keyword_containment, prove_memory_freshness
12
+
13
+
14
+ class ProveCommand:
15
+ """Generate zk proofs for memory properties."""
16
+
17
+ name = "prove"
18
+ help = "Prove a property of memory without revealing content (zk stub)"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "--memory", "-m", required=True, help="Memory file path (under current/)"
24
+ )
25
+ parser.add_argument(
26
+ "--property",
27
+ "-p",
28
+ choices=["keyword", "freshness"],
29
+ required=True,
30
+ help="Property to prove",
31
+ )
32
+ parser.add_argument("--value", "-v", help="Value (e.g. keyword or ISO date)")
33
+ parser.add_argument("--output", "-o", help="Output proof file path")
34
+
35
+ @staticmethod
36
+ def execute(args) -> int:
37
+ repo, code = require_repo()
38
+ if code != 0:
39
+ return code
40
+
41
+ path = repo.current_dir / args.memory
42
+ if not path.exists():
43
+ print(f"Memory file not found: {args.memory}")
44
+ return 1
45
+
46
+ out = args.output or "proof.bin"
47
+ out_path = Path(out)
48
+ if not out_path.is_absolute():
49
+ out_path = repo.root / out_path
50
+
51
+ if args.property == "keyword":
52
+ if not args.value:
53
+ print("--value required for keyword (the keyword)")
54
+ return 1
55
+ ok = prove_keyword_containment(path, args.value, out_path)
56
+ else:
57
+ if not args.value:
58
+ print("--value required for freshness (ISO date)")
59
+ return 1
60
+ ok = prove_memory_freshness(path, args.value, out_path)
61
+
62
+ if not ok:
63
+ print("Proof generation not yet implemented (zk backend required).")
64
+ return 1
65
+ print(f"Proof written to {out_path}")
66
+ return 0
memvcs/commands/pull.py CHANGED
@@ -25,6 +25,11 @@ class PullCommand:
25
25
  nargs="?",
26
26
  help="Branch to pull (default: all)",
27
27
  )
28
+ parser.add_argument(
29
+ "--yes",
30
+ action="store_true",
31
+ help="Accept conditionally trusted remote commits without prompting",
32
+ )
28
33
 
29
34
  @staticmethod
30
35
  def execute(args) -> int:
@@ -51,6 +56,28 @@ class PullCommand:
51
56
  remote_ref = f"{args.remote}/{current_branch}"
52
57
  remote_hash = repo.resolve_ref(remote_ref)
53
58
  if remote_hash:
59
+ from memvcs.core.crypto_verify import verify_commit_optional
60
+
61
+ verify_commit_optional(
62
+ repo.object_store, remote_hash, mem_dir=repo.mem_dir, strict=False
63
+ )
64
+ # Trust check: block or require confirmation for untrusted/conditional
65
+ from memvcs.core.objects import Commit
66
+ from memvcs.core.trust import find_verifying_key, get_trust_level
67
+
68
+ remote_commit = Commit.load(repo.object_store, remote_hash)
69
+ if remote_commit and remote_commit.metadata:
70
+ key_pem = find_verifying_key(repo.mem_dir, remote_commit.metadata)
71
+ if key_pem is not None:
72
+ level = get_trust_level(repo.mem_dir, key_pem)
73
+ if level == "untrusted":
74
+ print(f"Pull blocked: remote commit signed by untrusted key.")
75
+ return 1
76
+ if level == "conditional" and not getattr(args, "yes", False):
77
+ print(
78
+ "Remote commit from conditionally trusted key. Use --yes to merge."
79
+ )
80
+ return 1
54
81
  from memvcs.core.merge import MergeEngine
55
82
 
56
83
  merge_engine = MergeEngine(repo)
@@ -0,0 +1,130 @@
1
+ """
2
+ agmem resolve - Structured conflict resolution.
3
+
4
+ Resolve merge conflicts with ours/theirs/both choices; record in audit.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from ..commands.base import require_repo
13
+
14
+
15
+ def _path_under_current(path_str: str, current_dir: Path) -> Optional[Path]:
16
+ """Resolve path under current_dir; return None if it escapes (path traversal)."""
17
+ try:
18
+ full = (current_dir / path_str).resolve()
19
+ full.relative_to(current_dir.resolve())
20
+ return full
21
+ except (ValueError, RuntimeError):
22
+ return None
23
+
24
+
25
+ def _resolved_content(choice: str, ours: str, theirs: str) -> str:
26
+ """Return content for choice: ours, theirs, or both (merged)."""
27
+ if choice == "ours":
28
+ return ours
29
+ if choice == "theirs":
30
+ return theirs
31
+ return ours.rstrip() + "\n\n--- merged ---\n\n" + theirs
32
+
33
+
34
+ class ResolveCommand:
35
+ """Resolve merge conflicts interactively (ours/theirs/both)."""
36
+
37
+ name = "resolve"
38
+ help = "Resolve merge conflicts (ours / theirs / both)"
39
+
40
+ @staticmethod
41
+ def add_arguments(parser: argparse.ArgumentParser):
42
+ parser.add_argument(
43
+ "path",
44
+ nargs="?",
45
+ help="Conflict path to resolve (or resolve all if omitted)",
46
+ )
47
+ parser.add_argument(
48
+ "--ours",
49
+ action="store_true",
50
+ help="Resolve with our version",
51
+ )
52
+ parser.add_argument(
53
+ "--theirs",
54
+ action="store_true",
55
+ help="Resolve with their version",
56
+ )
57
+ parser.add_argument(
58
+ "--both",
59
+ action="store_true",
60
+ help="Keep both (append theirs after ours with separator)",
61
+ )
62
+
63
+ @staticmethod
64
+ def execute(args) -> int:
65
+ repo, code = require_repo()
66
+ if code != 0:
67
+ return code
68
+
69
+ merge_dir = repo.mem_dir / "merge"
70
+ conflicts_file = merge_dir / "conflicts.json"
71
+ if not conflicts_file.exists():
72
+ print("No unresolved conflicts.")
73
+ return 0
74
+
75
+ try:
76
+ conflicts = json.loads(conflicts_file.read_text())
77
+ except Exception:
78
+ print("Could not read conflicts file.")
79
+ return 1
80
+
81
+ if not conflicts:
82
+ print("No unresolved conflicts.")
83
+ conflicts_file.unlink(missing_ok=True)
84
+ return 0
85
+
86
+ choice = None
87
+ if args.ours:
88
+ choice = "ours"
89
+ elif args.theirs:
90
+ choice = "theirs"
91
+ elif args.both:
92
+ choice = "both"
93
+
94
+ resolved = 0
95
+ remaining = []
96
+ for c in conflicts:
97
+ path = c.get("path", "")
98
+ if args.path and path != args.path:
99
+ remaining.append(c)
100
+ continue
101
+ if choice is None:
102
+ print(f"Conflict: {path}")
103
+ print(" Use: agmem resolve <path> --ours | --theirs | --both")
104
+ remaining.append(c)
105
+ continue
106
+ ours_content = c.get("ours_content") or ""
107
+ theirs_content = c.get("theirs_content") or ""
108
+ full_path = _path_under_current(path, repo.current_dir)
109
+ if full_path is None:
110
+ print(f"Error: Conflict path escapes repository: {path}")
111
+ remaining.append(c)
112
+ continue
113
+ content = _resolved_content(choice, ours_content, theirs_content)
114
+ full_path.parent.mkdir(parents=True, exist_ok=True)
115
+ full_path.write_text(content, encoding="utf-8")
116
+ resolved += 1
117
+ try:
118
+ from ..core.audit import append_audit
119
+
120
+ append_audit(repo.mem_dir, "resolve", {"path": path, "choice": choice})
121
+ except Exception:
122
+ pass
123
+
124
+ if remaining:
125
+ conflicts_file.write_text(json.dumps(remaining, indent=2))
126
+ else:
127
+ conflicts_file.unlink(missing_ok=True)
128
+ if resolved:
129
+ print(f"Resolved {resolved} conflict(s). Stage and commit to complete.")
130
+ return 0
memvcs/commands/verify.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
- agmem verify - Belief consistency checker.
2
+ agmem verify - Belief consistency and cryptographic commit verification.
3
3
 
4
- Scans semantic memories for logical contradictions.
4
+ Scans semantic memories for logical contradictions; optionally verifies commit Merkle/signatures.
5
5
  """
6
6
 
7
7
  import argparse
@@ -12,10 +12,10 @@ from ..core.consistency import ConsistencyChecker, ConsistencyResult
12
12
 
13
13
 
14
14
  class VerifyCommand:
15
- """Verify belief consistency of semantic memories."""
15
+ """Verify belief consistency and/or cryptographic integrity of commits."""
16
16
 
17
17
  name = "verify"
18
- help = "Scan semantic memories for logical contradictions"
18
+ help = "Scan semantic memories for contradictions; optionally verify commit signatures"
19
19
 
20
20
  @staticmethod
21
21
  def add_arguments(parser: argparse.ArgumentParser):
@@ -23,8 +23,17 @@ class VerifyCommand:
23
23
  "--consistency",
24
24
  "-c",
25
25
  action="store_true",
26
- default=True,
27
- help="Check for contradictions (default)",
26
+ help="Check semantic memories for contradictions",
27
+ )
28
+ parser.add_argument(
29
+ "--crypto",
30
+ action="store_true",
31
+ help="Verify Merkle tree and signatures for commits",
32
+ )
33
+ parser.add_argument(
34
+ "--ref",
35
+ metavar="REF",
36
+ help="Commit or ref to verify (with --crypto); default HEAD",
28
37
  )
29
38
  parser.add_argument(
30
39
  "--llm",
@@ -32,28 +41,70 @@ class VerifyCommand:
32
41
  help="Use LLM for triple extraction (requires OpenAI)",
33
42
  )
34
43
 
44
+ @staticmethod
45
+ def _run_crypto_verify(repo, ref: str = None) -> int:
46
+ """Run cryptographic verification. Returns 0 if all OK, 1 on failure."""
47
+ from ..core.crypto_verify import verify_commit, load_public_key
48
+
49
+ if ref:
50
+ commit_hash = repo.resolve_ref(ref)
51
+ if not commit_hash:
52
+ print(f"Ref not found: {ref}")
53
+ return 1
54
+ else:
55
+ head = repo.refs.get_head()
56
+ if head["type"] == "branch":
57
+ commit_hash = repo.refs.get_branch_commit(head["value"])
58
+ else:
59
+ commit_hash = head.get("value")
60
+ if not commit_hash:
61
+ print("No commit to verify (empty repo).")
62
+ return 0
63
+ pub = load_public_key(repo.mem_dir)
64
+ ok, err = verify_commit(
65
+ repo.object_store, commit_hash, public_key_pem=pub, mem_dir=repo.mem_dir
66
+ )
67
+ if ok:
68
+ print(f"Commit {commit_hash[:8]} verified (Merkle + signature OK).")
69
+ return 0
70
+ print(f"Commit {commit_hash[:8]} verification failed: {err}")
71
+ return 1
72
+
35
73
  @staticmethod
36
74
  def execute(args) -> int:
37
75
  repo, code = require_repo()
38
76
  if code != 0:
39
77
  return code
40
78
 
41
- checker = ConsistencyChecker(repo, llm_provider="openai" if args.llm else None)
42
- result = checker.check(use_llm=args.llm)
79
+ run_consistency = args.consistency
80
+ run_crypto = args.crypto
81
+ if not run_consistency and not run_crypto:
82
+ run_consistency = True
43
83
 
44
- print(f"Checked {result.files_checked} semantic file(s)")
45
- if result.valid:
46
- print("No contradictions found.")
47
- return 0
84
+ exit_code = 0
48
85
 
49
- print(f"\nFound {len(result.contradictions)} contradiction(s):")
50
- for i, c in enumerate(result.contradictions, 1):
51
- print(f"\n[{i}] {c.reason}")
52
- print(
53
- f" {c.triple1.source}:{c.triple1.line}: {c.triple1.subject} {c.triple1.predicate} {c.triple1.obj}"
54
- )
55
- print(
56
- f" {c.triple2.source}:{c.triple2.line}: {c.triple2.subject} {c.triple2.predicate} {c.triple2.obj}"
57
- )
58
- print("\nUse 'agmem repair --strategy confidence' to attempt auto-fix.")
59
- return 1
86
+ if run_crypto:
87
+ if VerifyCommand._run_crypto_verify(repo, args.ref) != 0:
88
+ exit_code = 1
89
+
90
+ if run_consistency:
91
+ checker = ConsistencyChecker(repo, llm_provider="openai" if args.llm else None)
92
+ result = checker.check(use_llm=args.llm)
93
+
94
+ print(f"Checked {result.files_checked} semantic file(s)")
95
+ if result.valid:
96
+ print("No contradictions found.")
97
+ else:
98
+ exit_code = 1
99
+ print(f"\nFound {len(result.contradictions)} contradiction(s):")
100
+ for i, c in enumerate(result.contradictions, 1):
101
+ print(f"\n[{i}] {c.reason}")
102
+ print(
103
+ f" {c.triple1.source}:{c.triple1.line}: {c.triple1.subject} {c.triple1.predicate} {c.triple1.obj}"
104
+ )
105
+ print(
106
+ f" {c.triple2.source}:{c.triple2.line}: {c.triple2.subject} {c.triple2.predicate} {c.triple2.obj}"
107
+ )
108
+ print("\nUse 'agmem repair --strategy confidence' to attempt auto-fix.")
109
+
110
+ return exit_code