agmem 0.1.1__py3-none-any.whl → 0.1.2__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 (80) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
  2. agmem-0.1.2.dist-info/RECORD +86 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +35 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +77 -76
  7. memvcs/commands/blame.py +46 -53
  8. memvcs/commands/branch.py +13 -33
  9. memvcs/commands/checkout.py +27 -32
  10. memvcs/commands/clean.py +18 -23
  11. memvcs/commands/clone.py +4 -1
  12. memvcs/commands/commit.py +40 -39
  13. memvcs/commands/daemon.py +81 -76
  14. memvcs/commands/decay.py +77 -0
  15. memvcs/commands/diff.py +56 -57
  16. memvcs/commands/distill.py +74 -0
  17. memvcs/commands/fsck.py +55 -61
  18. memvcs/commands/garden.py +28 -37
  19. memvcs/commands/graph.py +41 -48
  20. memvcs/commands/init.py +16 -24
  21. memvcs/commands/log.py +25 -40
  22. memvcs/commands/merge.py +16 -28
  23. memvcs/commands/pack.py +129 -0
  24. memvcs/commands/pull.py +4 -1
  25. memvcs/commands/push.py +4 -2
  26. memvcs/commands/recall.py +145 -0
  27. memvcs/commands/reflog.py +13 -22
  28. memvcs/commands/remote.py +1 -0
  29. memvcs/commands/repair.py +66 -0
  30. memvcs/commands/reset.py +23 -33
  31. memvcs/commands/resurrect.py +82 -0
  32. memvcs/commands/search.py +3 -4
  33. memvcs/commands/serve.py +2 -1
  34. memvcs/commands/show.py +66 -36
  35. memvcs/commands/stash.py +34 -34
  36. memvcs/commands/status.py +27 -35
  37. memvcs/commands/tag.py +23 -47
  38. memvcs/commands/test.py +30 -44
  39. memvcs/commands/timeline.py +111 -0
  40. memvcs/commands/tree.py +26 -27
  41. memvcs/commands/verify.py +59 -0
  42. memvcs/commands/when.py +115 -0
  43. memvcs/core/access_index.py +167 -0
  44. memvcs/core/config_loader.py +3 -1
  45. memvcs/core/consistency.py +214 -0
  46. memvcs/core/decay.py +185 -0
  47. memvcs/core/diff.py +158 -143
  48. memvcs/core/distiller.py +277 -0
  49. memvcs/core/gardener.py +164 -132
  50. memvcs/core/hooks.py +48 -14
  51. memvcs/core/knowledge_graph.py +134 -138
  52. memvcs/core/merge.py +248 -171
  53. memvcs/core/objects.py +95 -96
  54. memvcs/core/pii_scanner.py +147 -146
  55. memvcs/core/refs.py +132 -115
  56. memvcs/core/repository.py +174 -164
  57. memvcs/core/schema.py +155 -113
  58. memvcs/core/staging.py +60 -65
  59. memvcs/core/storage/__init__.py +20 -18
  60. memvcs/core/storage/base.py +74 -70
  61. memvcs/core/storage/gcs.py +70 -68
  62. memvcs/core/storage/local.py +42 -40
  63. memvcs/core/storage/s3.py +105 -110
  64. memvcs/core/temporal_index.py +112 -0
  65. memvcs/core/test_runner.py +101 -93
  66. memvcs/core/vector_store.py +41 -35
  67. memvcs/integrations/mcp_server.py +1 -3
  68. memvcs/integrations/web_ui/server.py +25 -26
  69. memvcs/retrieval/__init__.py +22 -0
  70. memvcs/retrieval/base.py +54 -0
  71. memvcs/retrieval/pack.py +128 -0
  72. memvcs/retrieval/recaller.py +105 -0
  73. memvcs/retrieval/strategies.py +314 -0
  74. memvcs/utils/__init__.py +3 -3
  75. memvcs/utils/helpers.py +52 -52
  76. agmem-0.1.1.dist-info/RECORD +0 -67
  77. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
  78. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
  79. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
  80. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
memvcs/commands/log.py CHANGED
@@ -11,39 +11,24 @@ from ..core.repository import Repository
11
11
 
12
12
  class LogCommand:
13
13
  """Show commit history."""
14
-
15
- name = 'log'
16
- help = 'Show commit history'
17
-
14
+
15
+ name = "log"
16
+ help = "Show commit history"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
20
  parser.add_argument(
21
- '--max-count', '-n',
22
- type=int,
23
- default=10,
24
- help='Maximum number of commits to show'
25
- )
26
- parser.add_argument(
27
- '--oneline',
28
- action='store_true',
29
- help='Show one commit per line'
21
+ "--max-count", "-n", type=int, default=10, help="Maximum number of commits to show"
30
22
  )
23
+ parser.add_argument("--oneline", action="store_true", help="Show one commit per line")
31
24
  parser.add_argument(
32
- '--graph',
33
- action='store_true',
34
- help='Show ASCII graph of branch/merge history'
25
+ "--graph", action="store_true", help="Show ASCII graph of branch/merge history"
35
26
  )
27
+ parser.add_argument("--all", action="store_true", help="Show all branches")
36
28
  parser.add_argument(
37
- '--all',
38
- action='store_true',
39
- help='Show all branches'
29
+ "ref", nargs="?", help="Start from this reference (branch, tag, or commit)"
40
30
  )
41
- parser.add_argument(
42
- 'ref',
43
- nargs='?',
44
- help='Start from this reference (branch, tag, or commit)'
45
- )
46
-
31
+
47
32
  @staticmethod
48
33
  def execute(args) -> int:
49
34
  repo, code = require_repo()
@@ -52,11 +37,11 @@ class LogCommand:
52
37
 
53
38
  # Get commits
54
39
  commits = repo.get_log(max_count=args.max_count)
55
-
40
+
56
41
  if not commits:
57
42
  print("No commits yet.")
58
43
  return 0
59
-
44
+
60
45
  if args.oneline:
61
46
  for commit in commits:
62
47
  print(f"{commit['short_hash']} {commit['message']}")
@@ -71,33 +56,33 @@ class LogCommand:
71
56
  for i, commit in enumerate(commits):
72
57
  if i > 0:
73
58
  print()
74
-
59
+
75
60
  # Commit header
76
61
  print(f"\033[33mcommit {commit['hash']}\033[0m")
77
-
62
+
78
63
  # Show branch info if this is HEAD
79
64
  head = repo.refs.get_head()
80
- if head['type'] == 'branch':
81
- head_commit = repo.refs.get_branch_commit(head['value'])
82
- if head_commit == commit['hash']:
65
+ if head["type"] == "branch":
66
+ head_commit = repo.refs.get_branch_commit(head["value"])
67
+ if head_commit == commit["hash"]:
83
68
  print(f"\033[36mHEAD -> {head['value']}\033[0m")
84
-
69
+
85
70
  # Author and date
86
71
  print(f"Author: {commit['author']}")
87
-
72
+
88
73
  # Format timestamp
89
74
  try:
90
- ts = commit['timestamp']
91
- if ts.endswith('Z'):
75
+ ts = commit["timestamp"]
76
+ if ts.endswith("Z"):
92
77
  ts = ts[:-1]
93
78
  dt = datetime.fromisoformat(ts)
94
- date_str = dt.strftime('%a %b %d %H:%M:%S %Y')
79
+ date_str = dt.strftime("%a %b %d %H:%M:%S %Y")
95
80
  print(f"Date: {date_str}")
96
- except:
81
+ except Exception:
97
82
  print(f"Date: {commit['timestamp']}")
98
-
83
+
99
84
  # Message
100
85
  print()
101
86
  print(f" {commit['message']}")
102
-
87
+
103
88
  return 0
memvcs/commands/merge.py CHANGED
@@ -11,31 +11,19 @@ from ..core.repository import Repository
11
11
 
12
12
  class MergeCommand:
13
13
  """Merge branches."""
14
-
15
- name = 'merge'
16
- help = 'Join two or more development histories together'
17
-
14
+
15
+ name = "merge"
16
+ help = "Join two or more development histories together"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument("branch", help="Branch to merge into current branch")
21
+ parser.add_argument("-m", "--message", help="Merge commit message")
20
22
  parser.add_argument(
21
- 'branch',
22
- help='Branch to merge into current branch'
23
- )
24
- parser.add_argument(
25
- '-m', '--message',
26
- help='Merge commit message'
27
- )
28
- parser.add_argument(
29
- '--no-commit',
30
- action='store_true',
31
- help='Perform merge but do not commit'
32
- )
33
- parser.add_argument(
34
- '--abort',
35
- action='store_true',
36
- help='Abort the current merge'
23
+ "--no-commit", action="store_true", help="Perform merge but do not commit"
37
24
  )
38
-
25
+ parser.add_argument("--abort", action="store_true", help="Abort the current merge")
26
+
39
27
  @staticmethod
40
28
  def execute(args) -> int:
41
29
  repo, code = require_repo()
@@ -47,28 +35,28 @@ class MergeCommand:
47
35
  # TODO: Implement merge abort
48
36
  print("Merge abort not yet implemented")
49
37
  return 0
50
-
38
+
51
39
  # Check if we're on a branch
52
40
  current_branch = repo.refs.get_current_branch()
53
41
  if not current_branch:
54
42
  print("Error: Not currently on any branch.")
55
43
  print("Cannot merge when HEAD is detached.")
56
44
  return 1
57
-
45
+
58
46
  # Check if trying to merge current branch
59
47
  if args.branch == current_branch:
60
48
  print(f"Error: Cannot merge '{args.branch}' into itself")
61
49
  return 1
62
-
50
+
63
51
  # Check if branch exists
64
52
  if not repo.refs.branch_exists(args.branch):
65
53
  print(f"Error: Branch '{args.branch}' not found.")
66
54
  return 1
67
-
55
+
68
56
  # Perform merge
69
57
  engine = MergeEngine(repo)
70
58
  result = engine.merge(args.branch, message=args.message)
71
-
59
+
72
60
  if result.success:
73
61
  print(f"Merge successful: {result.message}")
74
62
  if result.commit_hash:
@@ -76,7 +64,7 @@ class MergeCommand:
76
64
  return 0
77
65
  else:
78
66
  print(f"Merge failed: {result.message}")
79
-
67
+
80
68
  if result.conflicts:
81
69
  print()
82
70
  print("Conflicts:")
@@ -84,5 +72,5 @@ class MergeCommand:
84
72
  print(f" {conflict.path}")
85
73
  print()
86
74
  print("Resolve conflicts and run 'agmem commit' to complete the merge.")
87
-
75
+
88
76
  return 1
@@ -0,0 +1,129 @@
1
+ """
2
+ agmem pack - Context window budget manager.
3
+
4
+ Packs recalled memories into token budget for LLM injection.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from ..commands.base import require_repo
12
+ from ..core.access_index import AccessIndex
13
+ from ..retrieval import RecallEngine
14
+ from ..retrieval.pack import PackEngine
15
+
16
+
17
+ class PackCommand:
18
+ """Context window budget manager."""
19
+
20
+ name = "pack"
21
+ help = "Pack recalled memories into token budget for LLM context"
22
+
23
+ @staticmethod
24
+ def add_arguments(parser: argparse.ArgumentParser):
25
+ parser.add_argument(
26
+ "--context",
27
+ "-c",
28
+ default="",
29
+ help="Current task description for recall",
30
+ )
31
+ parser.add_argument(
32
+ "--budget",
33
+ "-b",
34
+ type=int,
35
+ default=4000,
36
+ help="Max tokens (default: 4000)",
37
+ )
38
+ parser.add_argument(
39
+ "--strategy",
40
+ "-s",
41
+ choices=["relevance", "recency", "importance", "balanced"],
42
+ default="relevance",
43
+ help="Packing strategy (default: relevance)",
44
+ )
45
+ parser.add_argument(
46
+ "--exclude",
47
+ "-e",
48
+ action="append",
49
+ default=[],
50
+ help="Paths to exclude; repeatable",
51
+ )
52
+ parser.add_argument(
53
+ "--model",
54
+ "-m",
55
+ default="gpt-4o-mini",
56
+ help="Model for token counting (default: gpt-4o-mini)",
57
+ )
58
+ parser.add_argument(
59
+ "--format",
60
+ "-f",
61
+ choices=["text", "json"],
62
+ default="text",
63
+ help="Output format (default: text)",
64
+ )
65
+
66
+ @staticmethod
67
+ def execute(args) -> int:
68
+ repo, code = require_repo()
69
+ if code != 0:
70
+ return code
71
+
72
+ vector_store = None
73
+ try:
74
+ from ..core.vector_store import VectorStore
75
+
76
+ vs = VectorStore(repo.mem_dir)
77
+ vs._get_connection() # ensure sqlite-vec is usable; may raise
78
+ vector_store = vs
79
+ except Exception:
80
+ pass
81
+
82
+ access_index = AccessIndex(repo.mem_dir)
83
+ recall_engine = RecallEngine(
84
+ repo=repo,
85
+ vector_store=vector_store,
86
+ access_index=access_index,
87
+ use_cache=True,
88
+ )
89
+
90
+ strategy = "hybrid" if args.strategy == "balanced" else args.strategy
91
+ pack_engine = PackEngine(
92
+ recall_engine=recall_engine,
93
+ model=args.model,
94
+ summarization_cascade=False,
95
+ )
96
+
97
+ result = pack_engine.pack(
98
+ context=args.context,
99
+ budget=args.budget,
100
+ strategy=strategy,
101
+ exclude=args.exclude,
102
+ )
103
+
104
+ if args.format == "json":
105
+ import json
106
+
107
+ print(
108
+ json.dumps(
109
+ {
110
+ "content": result.content,
111
+ "total_tokens": result.total_tokens,
112
+ "budget": result.budget,
113
+ "items_used": result.items_used,
114
+ "items_total": result.items_total,
115
+ },
116
+ indent=2,
117
+ )
118
+ )
119
+ else:
120
+ print(result.content)
121
+ print(
122
+ f"\n# Pack stats: {result.total_tokens}/{result.budget} tokens, "
123
+ f"{result.items_used}/{result.items_total} items",
124
+ file=sys.stderr,
125
+ )
126
+
127
+ if vector_store and hasattr(vector_store, "close"):
128
+ vector_store.close()
129
+ return 0
memvcs/commands/pull.py CHANGED
@@ -37,7 +37,9 @@ class PullCommand:
37
37
 
38
38
  remote = Remote(repo.root, args.remote)
39
39
  if not remote.get_remote_url():
40
- print(f"Error: Remote '{args.remote}' has no URL. Set with: agmem remote add {args.remote} <url>")
40
+ print(
41
+ f"Error: Remote '{args.remote}' has no URL. Set with: agmem remote add {args.remote} <url>"
42
+ )
41
43
  return 1
42
44
 
43
45
  try:
@@ -50,6 +52,7 @@ class PullCommand:
50
52
  remote_hash = repo.resolve_ref(remote_ref)
51
53
  if remote_hash:
52
54
  from memvcs.core.merge import MergeEngine
55
+
53
56
  merge_engine = MergeEngine(repo)
54
57
  try:
55
58
  result = merge_engine.merge(remote_ref)
memvcs/commands/push.py CHANGED
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
 
9
9
  class MemoryConflictError(Exception):
10
10
  """Exception raised when push fails due to conflicts."""
11
+
11
12
  pass
12
13
 
13
14
 
@@ -31,7 +32,8 @@ class PushCommand:
31
32
  help="Branch to push (default: current)",
32
33
  )
33
34
  parser.add_argument(
34
- "--force", "-f",
35
+ "--force",
36
+ "-f",
35
37
  action="store_true",
36
38
  help="Force push (WARNING: may overwrite remote changes)",
37
39
  )
@@ -53,7 +55,7 @@ class PushCommand:
53
55
 
54
56
  remote = Remote(repo.root, args.remote)
55
57
  remote_url = remote.get_remote_url()
56
-
58
+
57
59
  if not remote_url:
58
60
  print(f"Error: Remote '{args.remote}' has no URL.")
59
61
  print(f"Set with: agmem remote add {args.remote} <url>")
@@ -0,0 +1,145 @@
1
+ """
2
+ agmem recall - Context-aware retrieval with pluggable strategies.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.access_index import AccessIndex
12
+ from ..retrieval import RecallEngine
13
+
14
+
15
+ def _is_vector_unavailable_error(exc: Exception) -> bool:
16
+ """True if exception indicates vector deps are missing."""
17
+ msg = str(exc).lower()
18
+ return any(
19
+ key in msg
20
+ for key in ("sqlite-vec", "sentence-transformers", "vector search", "agmem[vector]")
21
+ )
22
+
23
+
24
+ class RecallCommand:
25
+ """Context-aware recall with pluggable strategies."""
26
+
27
+ name = "recall"
28
+ help = "Recall curated memories for the current task (context-aware retrieval)"
29
+
30
+ @staticmethod
31
+ def add_arguments(parser: argparse.ArgumentParser):
32
+ parser.add_argument(
33
+ "--context",
34
+ "-c",
35
+ default="",
36
+ help="Current task description (used for embedding similarity)",
37
+ )
38
+ parser.add_argument(
39
+ "--strategy",
40
+ "-s",
41
+ choices=["recency", "importance", "similarity", "hybrid"],
42
+ default="hybrid",
43
+ help="Recall strategy (default: hybrid)",
44
+ )
45
+ parser.add_argument(
46
+ "--limit",
47
+ "-n",
48
+ type=int,
49
+ default=10,
50
+ help="Max chunks to return (default: 10)",
51
+ )
52
+ parser.add_argument(
53
+ "--exclude",
54
+ "-e",
55
+ action="append",
56
+ default=[],
57
+ help="Tags/paths to exclude (e.g., experiment/*); repeatable",
58
+ )
59
+ parser.add_argument(
60
+ "--no-cache",
61
+ action="store_true",
62
+ help="Disable recall cache",
63
+ )
64
+ parser.add_argument(
65
+ "--format",
66
+ "-f",
67
+ choices=["json", "text"],
68
+ default="json",
69
+ help="Output format (default: json)",
70
+ )
71
+
72
+ @staticmethod
73
+ def execute(args) -> int:
74
+ repo, code = require_repo()
75
+ if code != 0:
76
+ return code
77
+
78
+ vector_store = None
79
+ try:
80
+ from ..core.vector_store import VectorStore
81
+
82
+ vector_store = VectorStore(repo.mem_dir)
83
+ except ImportError:
84
+ if args.strategy in ("similarity", "hybrid"):
85
+ print(
86
+ "Error: Strategy '{}' requires agmem[vector]. "
87
+ "Install with: pip install agmem[vector]".format(args.strategy),
88
+ file=sys.stderr,
89
+ )
90
+ return 1
91
+ if args.strategy == "hybrid":
92
+ args.strategy = "recency"
93
+ print("Note: Falling back to recency (vector store not available)")
94
+
95
+ access_index = AccessIndex(repo.mem_dir)
96
+ engine = RecallEngine(
97
+ repo=repo,
98
+ vector_store=vector_store,
99
+ access_index=access_index,
100
+ use_cache=not args.no_cache,
101
+ )
102
+
103
+ try:
104
+ results = engine.recall(
105
+ context=args.context,
106
+ limit=args.limit,
107
+ strategy=args.strategy,
108
+ exclude=args.exclude,
109
+ )
110
+ except Exception as e:
111
+ if _is_vector_unavailable_error(e):
112
+ if args.strategy in ("similarity", "hybrid"):
113
+ print(
114
+ "Error: Vector search unavailable. Try --strategy recency or importance.",
115
+ file=sys.stderr,
116
+ )
117
+ return 1
118
+ engine = RecallEngine(
119
+ repo=repo,
120
+ vector_store=None,
121
+ access_index=access_index,
122
+ use_cache=not args.no_cache,
123
+ )
124
+ results = engine.recall(
125
+ context=args.context,
126
+ limit=args.limit,
127
+ strategy="recency",
128
+ exclude=args.exclude,
129
+ )
130
+ else:
131
+ raise
132
+
133
+ if args.format == "json":
134
+ output = [r.to_dict() for r in results]
135
+ print(json.dumps(output, indent=2))
136
+ else:
137
+ for r in results:
138
+ print(f"\n--- {r.path} (score: {r.relevance_score:.4f}) ---")
139
+ print(r.content[:500] + ("..." if len(r.content) > 500 else ""))
140
+ if r.importance is not None:
141
+ print(f"(importance: {r.importance})")
142
+
143
+ if vector_store and hasattr(vector_store, "close"):
144
+ vector_store.close()
145
+ return 0
memvcs/commands/reflog.py CHANGED
@@ -11,42 +11,33 @@ from ..core.repository import Repository
11
11
 
12
12
  class ReflogCommand:
13
13
  """Show reflog - history of HEAD changes."""
14
-
15
- name = 'reflog'
16
- help = 'Show reference log (history of HEAD changes)'
17
-
14
+
15
+ name = "reflog"
16
+ help = "Show reference log (history of HEAD changes)"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument("ref", nargs="?", default="HEAD", help="Reference to show log for")
20
21
  parser.add_argument(
21
- 'ref',
22
- nargs='?',
23
- default='HEAD',
24
- help='Reference to show log for'
25
- )
26
- parser.add_argument(
27
- '-n', '--max-count',
28
- type=int,
29
- default=20,
30
- help='Maximum number of entries'
22
+ "-n", "--max-count", type=int, default=20, help="Maximum number of entries"
31
23
  )
32
-
24
+
33
25
  @staticmethod
34
26
  def execute(args) -> int:
35
27
  repo, code = require_repo()
36
28
  if code != 0:
37
29
  return code
38
30
 
39
-
40
31
  entries = repo.refs.get_reflog(args.ref, args.max_count)
41
-
32
+
42
33
  if not entries:
43
34
  print("No reflog entries found.")
44
35
  return 0
45
-
36
+
46
37
  for e in entries:
47
- h = e['hash'][:8]
48
- ts = e.get('timestamp', '')[:19]
49
- msg = e.get('message', '')
38
+ h = e["hash"][:8]
39
+ ts = e.get("timestamp", "")[:19]
40
+ msg = e.get("message", "")
50
41
  print(f"{h} {ts} {msg}")
51
-
42
+
52
43
  return 0
memvcs/commands/remote.py CHANGED
@@ -40,6 +40,7 @@ class RemoteCommand:
40
40
  print(f"Remote '{args.name}' set to {args.url}")
41
41
  elif args.remote_action == "show":
42
42
  import json
43
+
43
44
  config = json.loads((repo.root / ".mem" / "config.json").read_text())
44
45
  remotes = config.get("remotes", {})
45
46
  if remotes:
@@ -0,0 +1,66 @@
1
+ """
2
+ agmem repair - Auto-fix belief contradictions.
3
+
4
+ Repairs semantic memory contradictions using configurable strategy.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.consistency import ConsistencyChecker, ConsistencyResult
12
+
13
+
14
+ class RepairCommand:
15
+ """Repair belief contradictions in semantic memories."""
16
+
17
+ name = "repair"
18
+ help = "Auto-fix contradictions using confidence scores"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "--strategy",
24
+ "-s",
25
+ choices=["confidence", "recency", "llm"],
26
+ default="confidence",
27
+ help="Repair strategy (default: confidence)",
28
+ )
29
+ parser.add_argument(
30
+ "--dry-run",
31
+ action="store_true",
32
+ help="Show what would be fixed without making changes",
33
+ )
34
+
35
+ @staticmethod
36
+ def execute(args) -> int:
37
+ repo, code = require_repo()
38
+ if code != 0:
39
+ return code
40
+
41
+ checker = ConsistencyChecker(repo, llm_provider="openai")
42
+ result = checker.repair(strategy=args.strategy)
43
+
44
+ if result.valid:
45
+ print("No contradictions to repair.")
46
+ return 0
47
+
48
+ if args.dry_run:
49
+ print(f"Would repair {len(result.contradictions)} contradiction(s):")
50
+ for c in result.contradictions:
51
+ print(
52
+ f" - {c.triple1.source}:{c.triple1.line} vs {c.triple2.source}:{c.triple2.line}"
53
+ )
54
+ print("\nRun without --dry-run to apply repairs.")
55
+ return 0
56
+
57
+ # Repair: keep higher-confidence triple, flag the other
58
+ # For now we only report - full repair would modify files
59
+ print(f"Found {len(result.contradictions)} contradiction(s).")
60
+ print("Manual repair required - edit the semantic files to resolve.")
61
+ for c in result.contradictions:
62
+ keep = c.triple1 if c.triple1.confidence >= c.triple2.confidence else c.triple2
63
+ drop = c.triple2 if keep is c.triple1 else c.triple1
64
+ print(f" Keep: {keep.source}:{keep.line} (confidence {keep.confidence})")
65
+ print(f" Review: {drop.source}:{drop.line} (confidence {drop.confidence})")
66
+ return 1