agmem 0.1.1__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.
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
- agmem-0.1.3.dist-info/RECORD +105 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +45 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +83 -76
- memvcs/commands/audit.py +59 -0
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +11 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +109 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +90 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +86 -61
- memvcs/commands/garden.py +40 -35
- memvcs/commands/gc.py +51 -0
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +69 -27
- memvcs/commands/pack.py +129 -0
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +31 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resolve.py +130 -0
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +110 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/audit.py +124 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +176 -145
- memvcs/core/hooks.py +48 -14
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +135 -138
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +260 -170
- memvcs/core/objects.py +110 -101
- memvcs/core/pack.py +92 -0
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/refs.py +132 -115
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +254 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +121 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +56 -36
- memvcs/core/zk_proofs.py +26 -0
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/commands/prove.py
ADDED
|
@@ -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:
|
|
@@ -37,7 +42,9 @@ class PullCommand:
|
|
|
37
42
|
|
|
38
43
|
remote = Remote(repo.root, args.remote)
|
|
39
44
|
if not remote.get_remote_url():
|
|
40
|
-
print(
|
|
45
|
+
print(
|
|
46
|
+
f"Error: Remote '{args.remote}' has no URL. Set with: agmem remote add {args.remote} <url>"
|
|
47
|
+
)
|
|
41
48
|
return 1
|
|
42
49
|
|
|
43
50
|
try:
|
|
@@ -49,7 +56,30 @@ class PullCommand:
|
|
|
49
56
|
remote_ref = f"{args.remote}/{current_branch}"
|
|
50
57
|
remote_hash = repo.resolve_ref(remote_ref)
|
|
51
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
|
|
52
81
|
from memvcs.core.merge import MergeEngine
|
|
82
|
+
|
|
53
83
|
merge_engine = MergeEngine(repo)
|
|
54
84
|
try:
|
|
55
85
|
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",
|
|
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 =
|
|
16
|
-
help =
|
|
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
|
-
|
|
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[
|
|
48
|
-
ts = e.get(
|
|
49
|
-
msg = e.get(
|
|
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
|
@@ -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
|
memvcs/commands/reset.py
CHANGED
|
@@ -11,34 +11,25 @@ from ..core.repository import Repository
|
|
|
11
11
|
|
|
12
12
|
class ResetCommand:
|
|
13
13
|
"""Reset current HEAD to the specified state."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "reset"
|
|
16
|
+
help = "Reset current HEAD to the specified state"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
20
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
nargs='?',
|
|
23
|
-
default='HEAD',
|
|
24
|
-
help='Commit to reset to (default: HEAD)'
|
|
21
|
+
"commit", nargs="?", default="HEAD", help="Commit to reset to (default: HEAD)"
|
|
25
22
|
)
|
|
26
23
|
parser.add_argument(
|
|
27
|
-
|
|
28
|
-
action='store_true',
|
|
29
|
-
help='Reset HEAD but keep staged changes'
|
|
24
|
+
"--soft", action="store_true", help="Reset HEAD but keep staged changes"
|
|
30
25
|
)
|
|
31
26
|
parser.add_argument(
|
|
32
|
-
|
|
33
|
-
action='store_true',
|
|
34
|
-
help='Reset HEAD and unstaged changes (default)'
|
|
27
|
+
"--mixed", action="store_true", help="Reset HEAD and unstaged changes (default)"
|
|
35
28
|
)
|
|
36
29
|
parser.add_argument(
|
|
37
|
-
|
|
38
|
-
action='store_true',
|
|
39
|
-
help='Reset HEAD, index, and working tree'
|
|
30
|
+
"--hard", action="store_true", help="Reset HEAD, index, and working tree"
|
|
40
31
|
)
|
|
41
|
-
|
|
32
|
+
|
|
42
33
|
@staticmethod
|
|
43
34
|
def execute(args) -> int:
|
|
44
35
|
# Find repository
|
|
@@ -46,53 +37,52 @@ class ResetCommand:
|
|
|
46
37
|
if code != 0:
|
|
47
38
|
return code
|
|
48
39
|
|
|
49
|
-
|
|
50
40
|
# Determine mode
|
|
51
41
|
if args.soft:
|
|
52
|
-
mode =
|
|
42
|
+
mode = "soft"
|
|
53
43
|
elif args.hard:
|
|
54
|
-
mode =
|
|
44
|
+
mode = "hard"
|
|
55
45
|
else:
|
|
56
|
-
mode =
|
|
57
|
-
|
|
46
|
+
mode = "mixed"
|
|
47
|
+
|
|
58
48
|
# Resolve commit
|
|
59
49
|
commit_hash = repo.resolve_ref(args.commit)
|
|
60
50
|
if not commit_hash:
|
|
61
51
|
print(f"Error: Unknown revision: {args.commit}")
|
|
62
52
|
return 1
|
|
63
|
-
|
|
53
|
+
|
|
64
54
|
# Get current branch
|
|
65
55
|
current_branch = repo.refs.get_current_branch()
|
|
66
|
-
|
|
56
|
+
|
|
67
57
|
try:
|
|
68
|
-
if mode ==
|
|
58
|
+
if mode == "soft":
|
|
69
59
|
# Just move HEAD
|
|
70
60
|
if current_branch:
|
|
71
61
|
repo.refs.set_branch_commit(current_branch, commit_hash)
|
|
72
62
|
else:
|
|
73
63
|
repo.refs.set_head_detached(commit_hash)
|
|
74
64
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
75
|
-
|
|
76
|
-
elif mode ==
|
|
65
|
+
|
|
66
|
+
elif mode == "mixed":
|
|
77
67
|
# Move HEAD and clear staging
|
|
78
68
|
if current_branch:
|
|
79
69
|
repo.refs.set_branch_commit(current_branch, commit_hash)
|
|
80
70
|
else:
|
|
81
71
|
repo.refs.set_head_detached(commit_hash)
|
|
82
|
-
|
|
72
|
+
|
|
83
73
|
# Keep staged files but mark them as unstaged
|
|
84
74
|
# (In a full implementation, we'd restore the tree state)
|
|
85
75
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
86
76
|
print("Staged changes have been unstaged.")
|
|
87
|
-
|
|
88
|
-
elif mode ==
|
|
77
|
+
|
|
78
|
+
elif mode == "hard":
|
|
89
79
|
# Move HEAD, clear staging, and restore working tree
|
|
90
80
|
repo.checkout(commit_hash, force=True)
|
|
91
81
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
92
82
|
print("Working tree has been reset.")
|
|
93
|
-
|
|
83
|
+
|
|
94
84
|
return 0
|
|
95
|
-
|
|
85
|
+
|
|
96
86
|
except Exception as e:
|
|
97
87
|
print(f"Error during reset: {e}")
|
|
98
88
|
return 1
|
|
@@ -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
|