nucleus-mcp 1.0.7 → 1.2.0
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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
BrainLock: The Atomic Safety Layer for Nucleus.
|
|
3
|
-
This module defines the locking primitives used to ensure data integrity across
|
|
4
|
-
multiple Nucleus processes (Daemon, CLI, MCP Server).
|
|
5
|
-
|
|
6
|
-
Strategic Role:
|
|
7
|
-
- Ensures "Atomic State" (The Brain doesn't hallucinate due to race conditions).
|
|
8
|
-
- Implements "Leased Locks" (Prevents deadlocks if a process crashes).
|
|
9
|
-
- Future-Proofing: Abstract base class allows swapping 'fcntl' for 'Redis' later.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import abc
|
|
13
|
-
import fcntl
|
|
14
|
-
import time
|
|
15
|
-
import os
|
|
16
|
-
import contextlib
|
|
17
|
-
import logging
|
|
18
|
-
import subprocess
|
|
19
|
-
import shutil
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Optional, Dict, Any, List
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
class BrainLock(abc.ABC):
|
|
26
|
-
"""
|
|
27
|
-
Abstract Base Class for Nucleus Locking.
|
|
28
|
-
Enforces the 'LockProvider' interface for Cloud/Local agnosticism.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
@abc.abstractmethod
|
|
32
|
-
def acquire(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None) -> bool:
|
|
33
|
-
"""Attempt to acquire the lock. Returns True if successful."""
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
@abc.abstractmethod
|
|
37
|
-
def release(self) -> None:
|
|
38
|
-
"""Release the lock."""
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
@abc.abstractmethod
|
|
42
|
-
def get_metadata(self) -> Dict[str, Any]:
|
|
43
|
-
"""Retrieve metadata associated with the lock."""
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
def check_stale_locks(self, max_age_seconds: float = 3600):
|
|
47
|
-
"""Optional: Check for and cleanup stale locks."""
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@contextlib.contextmanager
|
|
52
|
-
def section(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None):
|
|
53
|
-
"""Context manager for critical sections."""
|
|
54
|
-
acquired = self.acquire(timeout, metadata=metadata)
|
|
55
|
-
if not acquired:
|
|
56
|
-
raise TimeoutError(f"Could not acquire lock on {self} after {timeout}s")
|
|
57
|
-
try:
|
|
58
|
-
yield
|
|
59
|
-
finally:
|
|
60
|
-
self.release()
|
|
61
|
-
|
|
62
|
-
class FileBrainLock(BrainLock):
|
|
63
|
-
"""
|
|
64
|
-
Local Implementation using UNIX `fcntl` + `xattr` for metadata.
|
|
65
|
-
Used for the 'Local First' Sovereign OS.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
def __init__(self, lock_path: str):
|
|
69
|
-
self.lock_path = Path(lock_path)
|
|
70
|
-
self.lock_file = None
|
|
71
|
-
# Ensure directory exists
|
|
72
|
-
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
self.xattr_available = shutil.which("xattr") is not None
|
|
74
|
-
|
|
75
|
-
def _set_xattr(self, key: str, value: str):
|
|
76
|
-
"""Writes extended attribute if xattr is available."""
|
|
77
|
-
if not self.xattr_available:
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
subprocess.run(
|
|
82
|
-
["xattr", "-w", key, str(value), str(self.lock_path)],
|
|
83
|
-
check=True,
|
|
84
|
-
capture_output=True
|
|
85
|
-
)
|
|
86
|
-
except subprocess.CalledProcessError as e:
|
|
87
|
-
logger.warning(f"Failed to set xattr {key}: {e}")
|
|
88
|
-
|
|
89
|
-
def _get_xattr(self, key: str) -> Optional[str]:
|
|
90
|
-
"""Reads extended attribute if xattr is available."""
|
|
91
|
-
if not self.xattr_available:
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
result = subprocess.run(
|
|
96
|
-
["xattr", "-p", key, str(self.lock_path)],
|
|
97
|
-
check=True,
|
|
98
|
-
capture_output=True,
|
|
99
|
-
text=True
|
|
100
|
-
)
|
|
101
|
-
return result.stdout.strip()
|
|
102
|
-
except subprocess.CalledProcessError:
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
def _list_xattrs(self) -> List[str]:
|
|
106
|
-
"""Lists all xattrs on the file."""
|
|
107
|
-
if not self.xattr_available:
|
|
108
|
-
return []
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
result = subprocess.run(
|
|
112
|
-
["xattr", str(self.lock_path)],
|
|
113
|
-
check=True,
|
|
114
|
-
capture_output=True,
|
|
115
|
-
text=True
|
|
116
|
-
)
|
|
117
|
-
return result.stdout.strip().split("\n")
|
|
118
|
-
except subprocess.CalledProcessError:
|
|
119
|
-
return []
|
|
120
|
-
|
|
121
|
-
def acquire(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None) -> bool:
|
|
122
|
-
start_time = time.time()
|
|
123
|
-
|
|
124
|
-
# Open the file if not already open
|
|
125
|
-
if self.lock_file is None:
|
|
126
|
-
self.lock_file = open(self.lock_path, 'w+')
|
|
127
|
-
|
|
128
|
-
while True:
|
|
129
|
-
try:
|
|
130
|
-
# specific locking operation: LOCK_EX (Exclusive) | LOCK_NB (Non-Blocking)
|
|
131
|
-
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
132
|
-
|
|
133
|
-
# Write PID for debugging (knowing who holds the lock)
|
|
134
|
-
self.lock_file.seek(0)
|
|
135
|
-
self.lock_file.truncate()
|
|
136
|
-
self.lock_file.write(str(os.getpid()))
|
|
137
|
-
self.lock_file.flush()
|
|
138
|
-
|
|
139
|
-
# --- METADATA INJECTION (Phase 28) ---
|
|
140
|
-
if metadata:
|
|
141
|
-
# Always include timestamp if not present
|
|
142
|
-
if "timestamp" not in metadata:
|
|
143
|
-
metadata["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
144
|
-
|
|
145
|
-
for k, v in metadata.items():
|
|
146
|
-
# Namespace keys with 'nucleus.lock.' if not already
|
|
147
|
-
key = k if k.startswith("nucleus.lock.") else f"nucleus.lock.{k}"
|
|
148
|
-
self._set_xattr(key, v)
|
|
149
|
-
|
|
150
|
-
return True
|
|
151
|
-
except (IOError, OSError):
|
|
152
|
-
# Lock is held by another process
|
|
153
|
-
if time.time() - start_time > timeout:
|
|
154
|
-
logger.warning(f"Timeout waiting for lock: {self.lock_path}")
|
|
155
|
-
return False
|
|
156
|
-
time.sleep(0.1)
|
|
157
|
-
|
|
158
|
-
def release(self) -> None:
|
|
159
|
-
if self.lock_file:
|
|
160
|
-
try:
|
|
161
|
-
# Unlock
|
|
162
|
-
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
|
|
163
|
-
self.lock_file.close()
|
|
164
|
-
except ValueError:
|
|
165
|
-
# File might be closed already
|
|
166
|
-
pass
|
|
167
|
-
finally:
|
|
168
|
-
self.lock_file = None
|
|
169
|
-
|
|
170
|
-
def get_metadata(self) -> Dict[str, Any]:
|
|
171
|
-
"""Retrieves all nucleus.lock.* metadata from the lock file."""
|
|
172
|
-
if not self.lock_path.exists():
|
|
173
|
-
return {}
|
|
174
|
-
|
|
175
|
-
data = {}
|
|
176
|
-
# Get PID from file content
|
|
177
|
-
try:
|
|
178
|
-
with open(self.lock_path, 'r') as f:
|
|
179
|
-
data["pid"] = f.read().strip()
|
|
180
|
-
except Exception:
|
|
181
|
-
data["pid"] = "unknown"
|
|
182
|
-
|
|
183
|
-
# Get xattrs
|
|
184
|
-
if self.xattr_available:
|
|
185
|
-
attrs = self._list_xattrs()
|
|
186
|
-
for attr in attrs:
|
|
187
|
-
if attr.startswith("nucleus.lock."):
|
|
188
|
-
clean_key = attr.replace("nucleus.lock.", "")
|
|
189
|
-
data[clean_key] = self._get_xattr(attr)
|
|
190
|
-
|
|
191
|
-
return data
|
|
192
|
-
|
|
193
|
-
def check_stale_locks(self, max_age_seconds: float = 86400):
|
|
194
|
-
"""
|
|
195
|
-
Check for stale lock files.
|
|
196
|
-
Note: fcntl locks are released by OS on process death.
|
|
197
|
-
This simply cleans up old files to keep directory tidy.
|
|
198
|
-
"""
|
|
199
|
-
try:
|
|
200
|
-
if self.lock_path.exists():
|
|
201
|
-
stat = self.lock_path.stat()
|
|
202
|
-
age = time.time() - stat.st_mtime
|
|
203
|
-
if age > max_age_seconds:
|
|
204
|
-
# We could try to flock(LOCK_EX | LOCK_NB) to see if anyone holds it
|
|
205
|
-
# If we can lock it, it was truly stale (or just unused), so we can theoretically delete it.
|
|
206
|
-
# But dealing with race on deletion is tricky.
|
|
207
|
-
# For V1, we simply Log it.
|
|
208
|
-
logger.info(f"Old lock file detected: {self.lock_path} (Age: {age}s)")
|
|
209
|
-
except Exception:
|
|
210
|
-
pass
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def __repr__(self):
|
|
214
|
-
return f"<FileBrainLock: {self.lock_path}>"
|
|
215
|
-
|
|
216
|
-
# Factory Function for easy usage
|
|
217
|
-
def get_lock(resource_name: str, base_dir: Optional[Path] = None) -> BrainLock:
|
|
218
|
-
"""
|
|
219
|
-
Factory to get the appropriate lock for a resource.
|
|
220
|
-
Currently defaults to FileBrainLock.
|
|
221
|
-
"""
|
|
222
|
-
if base_dir is None:
|
|
223
|
-
# Default to a .locks directory in the user's home or project root
|
|
224
|
-
# For now, let's use the .brain/.locks directory if possible, or /tmp/nucleus
|
|
225
|
-
base_dir = Path(os.environ.get("NUCLEUS_LOCK_DIR", "/tmp/nucleus_locks"))
|
|
226
|
-
|
|
227
|
-
lock_path = base_dir / f"{resource_name}.lock"
|
|
228
|
-
return FileBrainLock(str(lock_path))
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import subprocess
|
|
3
|
-
import time
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Callable, Tuple, Dict, Any
|
|
7
|
-
|
|
8
|
-
class FixerLoop:
|
|
9
|
-
"""
|
|
10
|
-
Manages the 'Verify -> Diagnose -> Fix' cycle for autonomous code repair.
|
|
11
|
-
Phase 4: Self-Healing v2
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
def __init__(
|
|
15
|
-
self,
|
|
16
|
-
target_file: str,
|
|
17
|
-
verification_command: str,
|
|
18
|
-
fixer_func: Callable[[str, str], str],
|
|
19
|
-
max_retries: int = 3
|
|
20
|
-
):
|
|
21
|
-
self.target_file = Path(target_file)
|
|
22
|
-
self.verification_command = verification_command
|
|
23
|
-
self.fixer_func = fixer_func
|
|
24
|
-
self.max_retries = max_retries
|
|
25
|
-
self.logs = []
|
|
26
|
-
|
|
27
|
-
def _log(self, message: str):
|
|
28
|
-
timestamp = time.strftime("%H:%M:%S")
|
|
29
|
-
entry = f"[{timestamp}] {message}"
|
|
30
|
-
print(entry)
|
|
31
|
-
self.logs.append(entry)
|
|
32
|
-
|
|
33
|
-
def _run_verification(self) -> Tuple[bool, str]:
|
|
34
|
-
"""Runs the verification command and returns (success, output)."""
|
|
35
|
-
self._log(f"Running verification: {self.verification_command}")
|
|
36
|
-
try:
|
|
37
|
-
result = subprocess.run(
|
|
38
|
-
self.verification_command,
|
|
39
|
-
shell=True,
|
|
40
|
-
capture_output=True,
|
|
41
|
-
text=True,
|
|
42
|
-
timeout=60
|
|
43
|
-
)
|
|
44
|
-
output = result.stdout + "\n" + result.stderr
|
|
45
|
-
return (result.returncode == 0, output)
|
|
46
|
-
except subprocess.TimeoutExpired:
|
|
47
|
-
return (False, "Verification timed out after 60s")
|
|
48
|
-
except Exception as e:
|
|
49
|
-
return (False, f"Verification failed to run: {e}")
|
|
50
|
-
|
|
51
|
-
def run(self) -> Dict[str, Any]:
|
|
52
|
-
"""Execute the repair loop."""
|
|
53
|
-
|
|
54
|
-
if not self.target_file.exists():
|
|
55
|
-
return {
|
|
56
|
-
"status": "error",
|
|
57
|
-
"message": f"Target file {self.target_file} not found"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
self._log(f"Starting Fixer Loop for {self.target_file.name}")
|
|
61
|
-
|
|
62
|
-
# Initial Verification
|
|
63
|
-
success, output = self._run_verification()
|
|
64
|
-
if success:
|
|
65
|
-
self._log("Initial verification PASSED. No fix needed.")
|
|
66
|
-
return {
|
|
67
|
-
"status": "success",
|
|
68
|
-
"message": "File passed verification initially.",
|
|
69
|
-
"logs": self.logs
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
self._log("Initial verification FAILED. Starting repair cycles.")
|
|
73
|
-
|
|
74
|
-
for attempt in range(1, self.max_retries + 1):
|
|
75
|
-
self._log(f"--- Attempt {attempt}/{self.max_retries} ---")
|
|
76
|
-
|
|
77
|
-
# Diagnose & Fix
|
|
78
|
-
issues_context = f"Verification Command: {self.verification_command}\n\nOutput:\n{output[-2000:]}" # Truncate output
|
|
79
|
-
|
|
80
|
-
self._log("Invoking Fixer Agent...")
|
|
81
|
-
try:
|
|
82
|
-
# Calls brain_fix_code(path, context)
|
|
83
|
-
fix_result_json = self.fixer_func(str(self.target_file), issues_context)
|
|
84
|
-
fix_result = json.loads(fix_result_json)
|
|
85
|
-
|
|
86
|
-
if fix_result.get("status") == "error":
|
|
87
|
-
self._log(f"Fixer Agent Error: {fix_result.get('message')}")
|
|
88
|
-
# Continue anyway? No, probably fatal.
|
|
89
|
-
# But maybe transient.
|
|
90
|
-
except Exception as e:
|
|
91
|
-
self._log(f"Exception calling fixer: {e}")
|
|
92
|
-
|
|
93
|
-
# Verify Again
|
|
94
|
-
success, output = self._run_verification()
|
|
95
|
-
if success:
|
|
96
|
-
self._log("✅ Verification PASSED after fix.")
|
|
97
|
-
return {
|
|
98
|
-
"status": "success",
|
|
99
|
-
"message": f"Fixed in {attempt} attempts.",
|
|
100
|
-
"logs": self.logs
|
|
101
|
-
}
|
|
102
|
-
else:
|
|
103
|
-
self._log("❌ Verification FAILED. Retrying...")
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
"status": "failure",
|
|
107
|
-
"message": f"Failed to fix after {self.max_retries} attempts.",
|
|
108
|
-
"last_output": output,
|
|
109
|
-
"logs": self.logs
|
|
110
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
3
|
-
import subprocess
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Dict
|
|
6
|
-
import logging
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
|
-
# Configure logger
|
|
10
|
-
logger = logging.getLogger("nucleus.memory")
|
|
11
|
-
|
|
12
|
-
def get_brain_path() -> Path:
|
|
13
|
-
"""Get the brain path from environment variable."""
|
|
14
|
-
brain_path = os.environ.get("NUCLEAR_BRAIN_PATH")
|
|
15
|
-
if not brain_path:
|
|
16
|
-
# Fallback for dev environment
|
|
17
|
-
cwd = Path.cwd()
|
|
18
|
-
if (cwd / ".brain").exists():
|
|
19
|
-
return cwd / ".brain"
|
|
20
|
-
for parent in cwd.parents:
|
|
21
|
-
if (parent / ".brain").exists():
|
|
22
|
-
return parent / ".brain"
|
|
23
|
-
raise ValueError("NUCLEAR_BRAIN_PATH environment variable not set")
|
|
24
|
-
return Path(brain_path)
|
|
25
|
-
|
|
26
|
-
def _search_memory(query: str) -> Dict:
|
|
27
|
-
"""
|
|
28
|
-
Search the 'Long-term Memory' (artifacts/memory and ledger/decisions.md) using ripgrep.
|
|
29
|
-
"""
|
|
30
|
-
try:
|
|
31
|
-
brain = get_brain_path()
|
|
32
|
-
memory_dir = brain / "memory"
|
|
33
|
-
ledger_dir = brain / "ledger"
|
|
34
|
-
|
|
35
|
-
# Paths to search
|
|
36
|
-
search_paths = [str(memory_dir)]
|
|
37
|
-
if (ledger_dir / "decisions.md").exists():
|
|
38
|
-
search_paths.append(str(ledger_dir / "decisions.md"))
|
|
39
|
-
|
|
40
|
-
# Run ripgrep
|
|
41
|
-
# -i: case insensitive
|
|
42
|
-
# -C 2: 2 lines of context
|
|
43
|
-
# --json: output as json
|
|
44
|
-
|
|
45
|
-
# V1: Simple Text Search
|
|
46
|
-
cmd_text = ["rg", "-i", "-n", "--no-heading", query] + search_paths
|
|
47
|
-
try:
|
|
48
|
-
result_text = subprocess.run(cmd_text, capture_output=True, text=True)
|
|
49
|
-
snippets = []
|
|
50
|
-
if result_text.stdout:
|
|
51
|
-
snippets = result_text.stdout.strip().splitlines()
|
|
52
|
-
except FileNotFoundError:
|
|
53
|
-
# Fallback when ripgrep is not available
|
|
54
|
-
snippets = []
|
|
55
|
-
lowered = query.lower()
|
|
56
|
-
for path_str in search_paths:
|
|
57
|
-
path = Path(path_str)
|
|
58
|
-
if path.is_file():
|
|
59
|
-
files = [path]
|
|
60
|
-
else:
|
|
61
|
-
files = list(path.glob("*.md")) + list(path.glob("*.txt"))
|
|
62
|
-
for file_path in files:
|
|
63
|
-
try:
|
|
64
|
-
for line in file_path.read_text().splitlines():
|
|
65
|
-
if lowered in line.lower():
|
|
66
|
-
snippets.append(f"{file_path}:{line}")
|
|
67
|
-
except Exception:
|
|
68
|
-
continue
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
"query": query,
|
|
72
|
-
"count": len(snippets),
|
|
73
|
-
"results": snippets[:20] # Limit to 20 results
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
except Exception as e:
|
|
77
|
-
logger.error(f"Memory search failed: {e}")
|
|
78
|
-
return {"error": str(e)}
|
|
79
|
-
|
|
80
|
-
def _read_memory(category: str) -> Dict:
|
|
81
|
-
"""
|
|
82
|
-
Read specific memory categories (context, patterns, learnings).
|
|
83
|
-
"""
|
|
84
|
-
try:
|
|
85
|
-
brain = get_brain_path()
|
|
86
|
-
memory_dir = brain / "memory"
|
|
87
|
-
|
|
88
|
-
allowed_files = {
|
|
89
|
-
"context": "context.md",
|
|
90
|
-
"patterns": "patterns.md",
|
|
91
|
-
"learnings": "learnings.md",
|
|
92
|
-
"decisions": "decisions.md" # Actually in ledger, but handled here
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if category not in allowed_files:
|
|
96
|
-
return {"error": f"Invalid category. Allowed: {list(allowed_files.keys())}"}
|
|
97
|
-
|
|
98
|
-
filename = allowed_files[category]
|
|
99
|
-
file_path = memory_dir / filename
|
|
100
|
-
|
|
101
|
-
if category == "decisions":
|
|
102
|
-
file_path = brain / "ledger" / "decisions.md"
|
|
103
|
-
|
|
104
|
-
if not file_path.exists():
|
|
105
|
-
return {"error": f"Memory file {filename} not found."}
|
|
106
|
-
|
|
107
|
-
content = file_path.read_text()
|
|
108
|
-
return {
|
|
109
|
-
"category": category,
|
|
110
|
-
"path": str(file_path),
|
|
111
|
-
"content": content
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
except Exception as e:
|
|
115
|
-
logger.error(f"Read memory failed: {e}")
|
|
116
|
-
return {"error": str(e)}
|
|
117
|
-
|
|
118
|
-
def _write_memory(content: str, category: str = "learnings") -> str:
|
|
119
|
-
"""
|
|
120
|
-
Write a memory engram to a specific category file in Markdown format.
|
|
121
|
-
"""
|
|
122
|
-
try:
|
|
123
|
-
brain = get_brain_path()
|
|
124
|
-
memory_dir = brain / "memory"
|
|
125
|
-
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
-
|
|
127
|
-
allowed_files = {
|
|
128
|
-
"context": "context.md",
|
|
129
|
-
"patterns": "patterns.md",
|
|
130
|
-
"learnings": "learnings.md",
|
|
131
|
-
"decisions": "decisions.md"
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
# Default to learnings if unknown category, or map "memory" to learnings
|
|
135
|
-
if category == "memory":
|
|
136
|
-
category = "learnings"
|
|
137
|
-
|
|
138
|
-
target_file = allowed_files.get(category, "learnings.md")
|
|
139
|
-
|
|
140
|
-
# Special case for decisions (in ledger)
|
|
141
|
-
if category == "decisions":
|
|
142
|
-
file_path = brain / "ledger" / "decisions.md"
|
|
143
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
-
else:
|
|
145
|
-
file_path = memory_dir / target_file
|
|
146
|
-
|
|
147
|
-
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
148
|
-
entry = f"\n\n## [{timestamp}] Engram\n{content}"
|
|
149
|
-
|
|
150
|
-
mode = "a" if file_path.exists() else "w"
|
|
151
|
-
with open(file_path, mode) as f:
|
|
152
|
-
f.write(entry)
|
|
153
|
-
|
|
154
|
-
return f"Memory written to {target_file}"
|
|
155
|
-
|
|
156
|
-
except Exception as e:
|
|
157
|
-
logger.error(f"Write memory failed: {e}")
|
|
158
|
-
return f"Error writing memory: {str(e)}"
|