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,841 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Commitment Ledger - Core module for PEFS Phase 2
|
|
4
|
-
Tracks all commitments, aging, context, and closure methods
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import subprocess
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Dict, List, Optional
|
|
12
|
-
from .runtime.storage import read_brain_file, write_brain_file, brain_file_exists
|
|
13
|
-
from .runtime.locking import get_lock
|
|
14
|
-
|
|
15
|
-
def get_ledger_path(brain_path: Path) -> Path:
|
|
16
|
-
"""Get path to commitment ledger"""
|
|
17
|
-
return brain_path / "commitments" / "ledger.json"
|
|
18
|
-
|
|
19
|
-
def load_ledger(brain_path: Path) -> Dict:
|
|
20
|
-
"""Load commitment ledger from disk"""
|
|
21
|
-
ledger_path = get_ledger_path(brain_path)
|
|
22
|
-
|
|
23
|
-
if brain_file_exists(ledger_path):
|
|
24
|
-
return json.loads(read_brain_file(ledger_path))
|
|
25
|
-
|
|
26
|
-
# Default empty ledger
|
|
27
|
-
return {
|
|
28
|
-
"commitments": [],
|
|
29
|
-
"last_scan": None,
|
|
30
|
-
"last_interaction": None, # MDR_010: Usage telemetry
|
|
31
|
-
"notifications_sent": 0, # MDR_010: Value Ratio denominator
|
|
32
|
-
"high_impact_closed": 0, # MDR_010: Value Ratio numerator
|
|
33
|
-
"notifications_paused": False, # MDR_010: Kill Switch
|
|
34
|
-
"manual_overrides_count": 0, # MDR_010: Tracks when user fights the system
|
|
35
|
-
"estimated_time_saved_minutes": 0, # MDR_010: Accumulated time saved estimate
|
|
36
|
-
"stats": {
|
|
37
|
-
"total_open": 0,
|
|
38
|
-
"green_tier": 0,
|
|
39
|
-
"yellow_tier": 0,
|
|
40
|
-
"red_tier": 0
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
def save_ledger(brain_path: Path, ledger: Dict) -> None:
|
|
45
|
-
"""Save commitment ledger to disk"""
|
|
46
|
-
ledger_path = get_ledger_path(brain_path)
|
|
47
|
-
write_brain_file(ledger_path, json.dumps(ledger, indent=2))
|
|
48
|
-
|
|
49
|
-
def analyze_context(description: str, source_file: str) -> Dict:
|
|
50
|
-
"""
|
|
51
|
-
Analyze commitment context for routing decisions.
|
|
52
|
-
Returns novelty, dopamine, urgency, emotional_load.
|
|
53
|
-
"""
|
|
54
|
-
desc_lower = description.lower()
|
|
55
|
-
|
|
56
|
-
# Novelty detection
|
|
57
|
-
if any(k in desc_lower for k in ["first time", "new", "never"]):
|
|
58
|
-
novelty = "high"
|
|
59
|
-
elif any(k in desc_lower for k in ["repeat", "again", "another"]):
|
|
60
|
-
novelty = "low"
|
|
61
|
-
else:
|
|
62
|
-
novelty = "medium"
|
|
63
|
-
|
|
64
|
-
# Dopamine level (boring vs. fun tasks)
|
|
65
|
-
boring_keywords = ["fix", "bug", "setup", "config", "admin", "cron", "env"]
|
|
66
|
-
fun_keywords = ["build", "create", "design", "implement"]
|
|
67
|
-
|
|
68
|
-
if any(k in desc_lower for k in boring_keywords):
|
|
69
|
-
dopamine = "low"
|
|
70
|
-
elif any(k in desc_lower for k in fun_keywords):
|
|
71
|
-
dopamine = "high"
|
|
72
|
-
else:
|
|
73
|
-
dopamine = "medium"
|
|
74
|
-
|
|
75
|
-
# Urgency detection
|
|
76
|
-
if any(k in desc_lower for k in ["deadline", "urgent", "asap", "today"]):
|
|
77
|
-
urgency = "high"
|
|
78
|
-
elif any(k in desc_lower for k in ["soon", "this week"]):
|
|
79
|
-
urgency = "medium"
|
|
80
|
-
else:
|
|
81
|
-
urgency = None
|
|
82
|
-
|
|
83
|
-
# Emotional load (guilt triggers)
|
|
84
|
-
guilt_keywords = ["should", "need to", "must", "haven't", "forgot"]
|
|
85
|
-
emotional_load = sum(1 for k in guilt_keywords if k in desc_lower) * 2
|
|
86
|
-
emotional_load = min(emotional_load, 10) # Cap at 10
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
"novelty": novelty,
|
|
90
|
-
"dopamine": dopamine,
|
|
91
|
-
"urgency": urgency,
|
|
92
|
-
"emotional_load": emotional_load
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
def suggest_action(commitment: Dict) -> tuple[str, str]:
|
|
96
|
-
"""
|
|
97
|
-
Suggest closure action based on context and age.
|
|
98
|
-
Returns (action, reason) tuple.
|
|
99
|
-
"""
|
|
100
|
-
age = commitment["age_days"]
|
|
101
|
-
context = commitment["context"]
|
|
102
|
-
|
|
103
|
-
# Check learned patterns first
|
|
104
|
-
# We need to pass patterns in, or load them here (loading slightly inefficient but safe)
|
|
105
|
-
# Ideally suggest_action would take patterns as arg
|
|
106
|
-
# For now, we'll do best effort detection if patterns exist nearby or just use heuristic
|
|
107
|
-
pass
|
|
108
|
-
|
|
109
|
-
# Archive old items
|
|
110
|
-
if age > 30:
|
|
111
|
-
return ("archive", f"Stale ({age} days old)")
|
|
112
|
-
|
|
113
|
-
# High novelty always needs Chairman
|
|
114
|
-
if context["novelty"] == "high":
|
|
115
|
-
return ("schedule", "High novelty, needs Chairman attention")
|
|
116
|
-
|
|
117
|
-
# Low dopamine tasks schedule for morning
|
|
118
|
-
if context["dopamine"] == "low" and age < 7:
|
|
119
|
-
return ("schedule", "Low dopamine task, schedule for morning energy")
|
|
120
|
-
|
|
121
|
-
# Urgent items
|
|
122
|
-
if context["urgency"] == "high":
|
|
123
|
-
return ("do_now", "Urgent deadline")
|
|
124
|
-
|
|
125
|
-
# Recent simple tasks
|
|
126
|
-
if age < 3 and context["novelty"] == "low":
|
|
127
|
-
return ("do_now", "Recent and simple, quick win")
|
|
128
|
-
|
|
129
|
-
# Default
|
|
130
|
-
return ("schedule", "Needs focused time")
|
|
131
|
-
|
|
132
|
-
def add_commitment(
|
|
133
|
-
brain_path: Path,
|
|
134
|
-
source_file: str,
|
|
135
|
-
source_line: int,
|
|
136
|
-
description: str,
|
|
137
|
-
comm_type: str,
|
|
138
|
-
source: str = "auto_detected",
|
|
139
|
-
priority: int = 3,
|
|
140
|
-
required_skills: List[str] = None
|
|
141
|
-
) -> Dict:
|
|
142
|
-
"""
|
|
143
|
-
Add a new commitment to the ledger.
|
|
144
|
-
Returns the created commitment.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
brain_path: Path to brain directory
|
|
148
|
-
source_file: File where commitment was found
|
|
149
|
-
source_line: Line number in source file
|
|
150
|
-
description: What the commitment is
|
|
151
|
-
comm_type: Type of commitment (task, todo, draft, decision)
|
|
152
|
-
source: Where it came from (auto_detected, manual, migrated)
|
|
153
|
-
priority: 1-5, lower is higher priority
|
|
154
|
-
required_skills: Skills needed to complete
|
|
155
|
-
"""
|
|
156
|
-
# Transaction: Read -> Modify -> Write protected by Lock
|
|
157
|
-
with get_lock("ledger", brain_path).section():
|
|
158
|
-
ledger = load_ledger(brain_path)
|
|
159
|
-
|
|
160
|
-
# Generate ID
|
|
161
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
162
|
-
comm_id = f"comm_{timestamp}_{len(ledger['commitments'])}"
|
|
163
|
-
|
|
164
|
-
# Analyze context
|
|
165
|
-
context = analyze_context(description, source_file)
|
|
166
|
-
|
|
167
|
-
# Create commitment
|
|
168
|
-
commitment = {
|
|
169
|
-
"id": comm_id,
|
|
170
|
-
"created": datetime.now().isoformat(),
|
|
171
|
-
"source_file": source_file,
|
|
172
|
-
"source_line": source_line,
|
|
173
|
-
"type": comm_type,
|
|
174
|
-
"source": source,
|
|
175
|
-
"description": description,
|
|
176
|
-
"context": context,
|
|
177
|
-
"age_days": 0,
|
|
178
|
-
"tier": "green",
|
|
179
|
-
"priority": priority,
|
|
180
|
-
"required_skills": required_skills or [],
|
|
181
|
-
"suggested_action": None,
|
|
182
|
-
"suggested_reason": None,
|
|
183
|
-
"status": "open",
|
|
184
|
-
"closed_at": None,
|
|
185
|
-
"closed_method": None
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
# Add suggested action
|
|
189
|
-
try:
|
|
190
|
-
# Re-load patterns inside lock if needed, but patterns file has its own lock ideally.
|
|
191
|
-
# For now, simplistic approach.
|
|
192
|
-
patterns = load_patterns(brain_path)
|
|
193
|
-
pattern_suggestion = suggest_pattern_action(commitment, patterns)
|
|
194
|
-
except Exception:
|
|
195
|
-
pattern_suggestion = None
|
|
196
|
-
|
|
197
|
-
if pattern_suggestion:
|
|
198
|
-
action, reason = pattern_suggestion
|
|
199
|
-
else:
|
|
200
|
-
action, reason = suggest_action(commitment)
|
|
201
|
-
|
|
202
|
-
commitment["suggested_action"] = action
|
|
203
|
-
commitment["suggested_reason"] = reason
|
|
204
|
-
|
|
205
|
-
ledger["commitments"].append(commitment)
|
|
206
|
-
save_ledger(brain_path, ledger)
|
|
207
|
-
|
|
208
|
-
return commitment
|
|
209
|
-
|
|
210
|
-
def update_commitment_ages(brain_path: Path) -> Dict:
|
|
211
|
-
"""
|
|
212
|
-
Update age and tier for all open commitments.
|
|
213
|
-
Returns updated ledger with stats.
|
|
214
|
-
"""
|
|
215
|
-
with get_lock("ledger", brain_path).section():
|
|
216
|
-
ledger = load_ledger(brain_path)
|
|
217
|
-
|
|
218
|
-
for comm in ledger["commitments"]:
|
|
219
|
-
if comm["status"] == "open":
|
|
220
|
-
# Calculate age
|
|
221
|
-
created = datetime.fromisoformat(comm["created"])
|
|
222
|
-
comm["age_days"] = (datetime.now() - created).days
|
|
223
|
-
|
|
224
|
-
# Update tier
|
|
225
|
-
if comm["age_days"] < 3:
|
|
226
|
-
comm["tier"] = "green"
|
|
227
|
-
elif comm["age_days"] < 7:
|
|
228
|
-
comm["tier"] = "yellow"
|
|
229
|
-
else:
|
|
230
|
-
comm["tier"] = "red"
|
|
231
|
-
|
|
232
|
-
# Update suggestion
|
|
233
|
-
action, reason = suggest_action(comm)
|
|
234
|
-
comm["suggested_action"] = action
|
|
235
|
-
comm["suggested_reason"] = reason
|
|
236
|
-
|
|
237
|
-
# Update stats
|
|
238
|
-
open_comms = [c for c in ledger["commitments"] if c["status"] == "open"]
|
|
239
|
-
|
|
240
|
-
# Count by type
|
|
241
|
-
by_type = {}
|
|
242
|
-
for c in open_comms:
|
|
243
|
-
t = c.get("type", "unknown")
|
|
244
|
-
by_type[t] = by_type.get(t, 0) + 1
|
|
245
|
-
|
|
246
|
-
ledger["stats"] = {
|
|
247
|
-
"total_open": len(open_comms),
|
|
248
|
-
"green_tier": len([c for c in open_comms if c["tier"] == "green"]),
|
|
249
|
-
"yellow_tier": len([c for c in open_comms if c["tier"] == "yellow"]),
|
|
250
|
-
"red_tier": len([c for c in open_comms if c["tier"] == "red"]),
|
|
251
|
-
"by_type": by_type
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
ledger["last_scan"] = datetime.now().isoformat()
|
|
255
|
-
save_ledger(brain_path, ledger)
|
|
256
|
-
|
|
257
|
-
return ledger
|
|
258
|
-
|
|
259
|
-
def close_commitment(
|
|
260
|
-
brain_path: Path,
|
|
261
|
-
comm_id: str,
|
|
262
|
-
method: str
|
|
263
|
-
) -> Dict:
|
|
264
|
-
"""
|
|
265
|
-
Close a commitment with specified method.
|
|
266
|
-
Returns updated commitment.
|
|
267
|
-
"""
|
|
268
|
-
with get_lock("ledger", brain_path).section():
|
|
269
|
-
ledger = load_ledger(brain_path)
|
|
270
|
-
|
|
271
|
-
for comm in ledger["commitments"]:
|
|
272
|
-
if comm["id"] == comm_id:
|
|
273
|
-
comm["status"] = "closed"
|
|
274
|
-
comm["closed_at"] = datetime.now().isoformat()
|
|
275
|
-
comm["closed_method"] = method
|
|
276
|
-
save_ledger(brain_path, ledger)
|
|
277
|
-
return comm
|
|
278
|
-
|
|
279
|
-
raise ValueError(f"Commitment {comm_id} not found")
|
|
280
|
-
|
|
281
|
-
# ============================================================
|
|
282
|
-
# WEEKLY CHALLENGE SYSTEM
|
|
283
|
-
# ============================================================
|
|
284
|
-
|
|
285
|
-
def get_challenge_path(brain_path: Path) -> Path:
|
|
286
|
-
"""Get path to weekly challenge file"""
|
|
287
|
-
return brain_path / "challenges" / "current_challenge.json"
|
|
288
|
-
|
|
289
|
-
def load_challenge(brain_path: Path) -> Dict:
|
|
290
|
-
"""Load current weekly challenge"""
|
|
291
|
-
path = get_challenge_path(brain_path)
|
|
292
|
-
if brain_file_exists(path):
|
|
293
|
-
return json.loads(read_brain_file(path))
|
|
294
|
-
return None
|
|
295
|
-
|
|
296
|
-
def set_challenge(brain_path: Path, challenge: Dict) -> None:
|
|
297
|
-
"""Set current weekly challenge"""
|
|
298
|
-
path = get_challenge_path(brain_path)
|
|
299
|
-
write_brain_file(path, json.dumps(challenge, indent=2))
|
|
300
|
-
|
|
301
|
-
def get_starter_challenges() -> List[Dict]:
|
|
302
|
-
"""Get list of starter challenges"""
|
|
303
|
-
return [
|
|
304
|
-
{
|
|
305
|
-
"id": "red_slayer",
|
|
306
|
-
"title": "Red Slayer",
|
|
307
|
-
"description": "Close 3 red-tier items this week",
|
|
308
|
-
"target_type": "tier",
|
|
309
|
-
"target_value": "red",
|
|
310
|
-
"target_count": 3,
|
|
311
|
-
"reward": "Guilt-free weekend"
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"id": "draft_finisher",
|
|
315
|
-
"title": "Draft Finisher",
|
|
316
|
-
"description": "Turn 2 drafts into published/final docs",
|
|
317
|
-
"target_type": "type",
|
|
318
|
-
"target_value": "draft",
|
|
319
|
-
"target_count": 2,
|
|
320
|
-
"reward": "Clarity boost"
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
"id": "frog_eater",
|
|
324
|
-
"title": "Frog Eater",
|
|
325
|
-
"description": "Close 1 item older than 20 days",
|
|
326
|
-
"target_type": "age",
|
|
327
|
-
"target_min_days": 20,
|
|
328
|
-
"target_count": 1,
|
|
329
|
-
"reward": "Massive dopamine hit"
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
"id": "inbox_zero",
|
|
333
|
-
"title": "Inbox Zero",
|
|
334
|
-
"description": "Get total open loops under 5",
|
|
335
|
-
"target_type": "total_count",
|
|
336
|
-
"target_max": 5,
|
|
337
|
-
"reward": "Zen mind"
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
"id": "quick_wins",
|
|
341
|
-
"title": "Quick Wins",
|
|
342
|
-
"description": "Close 5 green-tier items in one day",
|
|
343
|
-
"target_type": "velocity",
|
|
344
|
-
"target_count": 5,
|
|
345
|
-
"reward": "Momentum builder"
|
|
346
|
-
}
|
|
347
|
-
]
|
|
348
|
-
|
|
349
|
-
def check_challenge_progress(brain_path: Path) -> Dict:
|
|
350
|
-
"""Check progress on current challenge"""
|
|
351
|
-
challenge = load_challenge(brain_path)
|
|
352
|
-
if not challenge:
|
|
353
|
-
return None
|
|
354
|
-
|
|
355
|
-
load_ledger(brain_path)
|
|
356
|
-
|
|
357
|
-
# Future expansion: track closed items history
|
|
358
|
-
return {
|
|
359
|
-
"challenge": challenge,
|
|
360
|
-
"status": "in_progress",
|
|
361
|
-
"progress_note": "Tracking implemented in next iteration"
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
# ============================================================
|
|
365
|
-
# PATTERN MATCHING SYSTEM-
|
|
366
|
-
# ============================================================
|
|
367
|
-
|
|
368
|
-
def get_patterns_path(brain_path: Path) -> Path:
|
|
369
|
-
return brain_path / "patterns" / "learned_patterns.json"
|
|
370
|
-
|
|
371
|
-
def load_patterns(brain_path: Path) -> List[Dict]:
|
|
372
|
-
path = get_patterns_path(brain_path)
|
|
373
|
-
if brain_file_exists(path):
|
|
374
|
-
return json.loads(read_brain_file(path))
|
|
375
|
-
return []
|
|
376
|
-
|
|
377
|
-
def save_patterns(brain_path: Path, patterns: List[Dict]) -> None:
|
|
378
|
-
path = get_patterns_path(brain_path)
|
|
379
|
-
write_brain_file(path, json.dumps(patterns, indent=2))
|
|
380
|
-
|
|
381
|
-
def suggest_pattern_action(commitment: Dict, patterns: List[Dict]) -> Optional[tuple]:
|
|
382
|
-
"""Suggest action based on learned patterns"""
|
|
383
|
-
# Simple keyword matching for MVP
|
|
384
|
-
desc = commitment["description"].lower()
|
|
385
|
-
|
|
386
|
-
for pattern in patterns:
|
|
387
|
-
if any(k in desc for k in pattern["keywords"]):
|
|
388
|
-
return (pattern["action"], f"Pattern match: {pattern['name']}")
|
|
389
|
-
|
|
390
|
-
return None
|
|
391
|
-
|
|
392
|
-
def learn_patterns(brain_path: Path) -> List[Dict]:
|
|
393
|
-
"""Analyze closed commitments to learn patterns"""
|
|
394
|
-
ledger = load_ledger(brain_path)
|
|
395
|
-
closed = [c for c in ledger["commitments"] if c["status"] == "closed"]
|
|
396
|
-
patterns = load_patterns(brain_path)
|
|
397
|
-
|
|
398
|
-
# Simple heuristic: If we closed 3+ items with same keyword using same method
|
|
399
|
-
keywords = {}
|
|
400
|
-
for c in closed:
|
|
401
|
-
words = c["description"].lower().split()
|
|
402
|
-
method = c.get("closed_method", "unknown")
|
|
403
|
-
|
|
404
|
-
for w in words:
|
|
405
|
-
if len(w) > 4: # Skip short words
|
|
406
|
-
key = (w, method)
|
|
407
|
-
keywords[key] = keywords.get(key, 0) + 1
|
|
408
|
-
|
|
409
|
-
# Detect new patterns
|
|
410
|
-
new_found = 0
|
|
411
|
-
for (word, method), count in keywords.items():
|
|
412
|
-
if count >= 3:
|
|
413
|
-
# Check if exists
|
|
414
|
-
if not any(word in p["keywords"] and p["action"] == method for p in patterns):
|
|
415
|
-
patterns.append({
|
|
416
|
-
"name": f"Successful {word} via {method}",
|
|
417
|
-
"keywords": [word],
|
|
418
|
-
"action": method,
|
|
419
|
-
"confidence": 0.8,
|
|
420
|
-
"source": "auto_learned"
|
|
421
|
-
})
|
|
422
|
-
new_found += 1
|
|
423
|
-
|
|
424
|
-
if new_found > 0:
|
|
425
|
-
save_patterns(brain_path, patterns)
|
|
426
|
-
|
|
427
|
-
return patterns
|
|
428
|
-
|
|
429
|
-
# ============================================================
|
|
430
|
-
# COORDINATION METRICS
|
|
431
|
-
# ============================================================
|
|
432
|
-
|
|
433
|
-
def calculate_metrics(brain_path: Path) -> Dict:
|
|
434
|
-
"""Calculate coordination metrics (velocity, load, trends)"""
|
|
435
|
-
ledger = load_ledger(brain_path)
|
|
436
|
-
commitments = ledger["commitments"]
|
|
437
|
-
|
|
438
|
-
# 1. Closure Velocity (last 7 days)
|
|
439
|
-
closed_last_7d = []
|
|
440
|
-
now = datetime.now()
|
|
441
|
-
|
|
442
|
-
for c in commitments:
|
|
443
|
-
if c["status"] == "closed" and c["closed_at"]:
|
|
444
|
-
closed_time = datetime.fromisoformat(c["closed_at"])
|
|
445
|
-
if (now - closed_time).days <= 7:
|
|
446
|
-
closed_last_7d.append(c)
|
|
447
|
-
|
|
448
|
-
velocity = len(closed_last_7d)
|
|
449
|
-
|
|
450
|
-
# Avg days to close
|
|
451
|
-
avg_days_to_close = 0
|
|
452
|
-
if velocity > 0:
|
|
453
|
-
total_days = sum(c["age_days"] for c in closed_last_7d)
|
|
454
|
-
avg_days_to_close = round(total_days / velocity, 1)
|
|
455
|
-
|
|
456
|
-
# 2. Closure Rate by Type
|
|
457
|
-
by_type = {}
|
|
458
|
-
total_closed = len([c for c in commitments if c["status"] == "closed"])
|
|
459
|
-
|
|
460
|
-
if total_closed > 0:
|
|
461
|
-
for c in commitments:
|
|
462
|
-
if c["status"] == "closed":
|
|
463
|
-
t = c.get("type", "unknown")
|
|
464
|
-
by_type[t] = by_type.get(t, 0) + 1
|
|
465
|
-
|
|
466
|
-
# Convert to percentage
|
|
467
|
-
for t in by_type:
|
|
468
|
-
by_type[t] = f"{round((by_type[t] / total_closed) * 100)}%"
|
|
469
|
-
|
|
470
|
-
# 3. Mental Load Trend (Current snapshop vs History?)
|
|
471
|
-
# For MVP, we'll just return current load which nightly agent logs historically
|
|
472
|
-
stats = ledger.get("stats", {})
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
"velocity_7d": velocity,
|
|
476
|
-
"avg_days_to_close": avg_days_to_close,
|
|
477
|
-
"closure_rates": by_type,
|
|
478
|
-
"current_load": {
|
|
479
|
-
"total": stats.get("total_open", 0),
|
|
480
|
-
"red": stats.get("red_tier", 0)
|
|
481
|
-
},
|
|
482
|
-
# MDR_010: Value Ratio
|
|
483
|
-
"value_ratio": calculate_value_ratio(brain_path)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
# ============================================================
|
|
487
|
-
# MDR_010: USAGE TELEMETRY & FEEDBACK
|
|
488
|
-
# ============================================================
|
|
489
|
-
|
|
490
|
-
def record_interaction(brain_path: Path) -> None:
|
|
491
|
-
"""Record a user interaction timestamp (MDR_010)"""
|
|
492
|
-
with get_lock("ledger", brain_path).section():
|
|
493
|
-
ledger = load_ledger(brain_path)
|
|
494
|
-
ledger["last_interaction"] = datetime.now().isoformat()
|
|
495
|
-
save_ledger(brain_path, ledger)
|
|
496
|
-
|
|
497
|
-
def increment_notifications(brain_path: Path, count: int = 1) -> None:
|
|
498
|
-
"""Increment notifications sent counter (MDR_010)"""
|
|
499
|
-
with get_lock("ledger", brain_path).section():
|
|
500
|
-
ledger = load_ledger(brain_path)
|
|
501
|
-
ledger["notifications_sent"] = ledger.get("notifications_sent", 0) + count
|
|
502
|
-
save_ledger(brain_path, ledger)
|
|
503
|
-
|
|
504
|
-
def mark_high_impact_closure(brain_path: Path) -> None:
|
|
505
|
-
"""Increment high-impact closures counter (MDR_010)"""
|
|
506
|
-
with get_lock("ledger", brain_path).section():
|
|
507
|
-
ledger = load_ledger(brain_path)
|
|
508
|
-
ledger["high_impact_closed"] = ledger.get("high_impact_closed", 0) + 1
|
|
509
|
-
save_ledger(brain_path, ledger)
|
|
510
|
-
|
|
511
|
-
def calculate_value_ratio(brain_path: Path) -> Dict:
|
|
512
|
-
"""Calculate Value Ratio metric (MDR_010)"""
|
|
513
|
-
ledger = load_ledger(brain_path)
|
|
514
|
-
notifications = ledger.get("notifications_sent", 0)
|
|
515
|
-
high_impact = ledger.get("high_impact_closed", 0)
|
|
516
|
-
|
|
517
|
-
if notifications == 0:
|
|
518
|
-
ratio = None
|
|
519
|
-
verdict = "No notifications sent yet"
|
|
520
|
-
else:
|
|
521
|
-
ratio = round(high_impact / notifications, 3)
|
|
522
|
-
if ratio >= 0.2:
|
|
523
|
-
verdict = "✅ EXCELLENT (1 impact per 5 notifications)"
|
|
524
|
-
elif ratio >= 0.1:
|
|
525
|
-
verdict = "🟡 GOOD (1 impact per 10 notifications)"
|
|
526
|
-
elif ratio >= 0.01:
|
|
527
|
-
verdict = "🔴 POOR (1 impact per 100 notifications)"
|
|
528
|
-
else:
|
|
529
|
-
verdict = "❌ FAILURE - consider pausing notifications"
|
|
530
|
-
|
|
531
|
-
return {
|
|
532
|
-
"notifications_sent": notifications,
|
|
533
|
-
"high_impact_closed": high_impact,
|
|
534
|
-
"ratio": ratio,
|
|
535
|
-
"verdict": verdict
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
def record_manual_override(brain_path: Path, reason: str = "") -> None:
|
|
539
|
-
"""Record when user fights/overrides the system (MDR_010)"""
|
|
540
|
-
with get_lock("ledger", brain_path).section():
|
|
541
|
-
ledger = load_ledger(brain_path)
|
|
542
|
-
ledger["manual_overrides_count"] = ledger.get("manual_overrides_count", 0) + 1
|
|
543
|
-
save_ledger(brain_path, ledger)
|
|
544
|
-
|
|
545
|
-
def estimate_time_saved(brain_path: Path, minutes: int) -> None:
|
|
546
|
-
"""Add estimated time saved to running total (MDR_010)"""
|
|
547
|
-
with get_lock("ledger", brain_path).section():
|
|
548
|
-
ledger = load_ledger(brain_path)
|
|
549
|
-
ledger["estimated_time_saved_minutes"] = ledger.get("estimated_time_saved_minutes", 0) + minutes
|
|
550
|
-
save_ledger(brain_path, ledger)
|
|
551
|
-
|
|
552
|
-
def get_weekly_summary(brain_path: Path) -> Dict:
|
|
553
|
-
"""
|
|
554
|
-
Generate Sunday weekly summary with time saved (MDR_010).
|
|
555
|
-
Returns metrics for the weekly briefing.
|
|
556
|
-
"""
|
|
557
|
-
ledger = load_ledger(brain_path)
|
|
558
|
-
metrics = calculate_metrics(brain_path)
|
|
559
|
-
value_ratio = calculate_value_ratio(brain_path)
|
|
560
|
-
|
|
561
|
-
time_saved = ledger.get("estimated_time_saved_minutes", 0)
|
|
562
|
-
overrides = ledger.get("manual_overrides_count", 0)
|
|
563
|
-
|
|
564
|
-
# Estimate: each auto-archived item saves ~5 mins of mental overhead
|
|
565
|
-
auto_archive_savings = metrics.get("archived_count", 0) * 5
|
|
566
|
-
total_time_saved = time_saved + auto_archive_savings
|
|
567
|
-
|
|
568
|
-
return {
|
|
569
|
-
"velocity_7d": metrics.get("velocity_7d", 0),
|
|
570
|
-
"avg_days_to_close": metrics.get("avg_days_to_close", 0),
|
|
571
|
-
"value_ratio": value_ratio,
|
|
572
|
-
"estimated_time_saved_hours": round(total_time_saved / 60, 1),
|
|
573
|
-
"manual_overrides": overrides,
|
|
574
|
-
"friction_score": "HIGH" if overrides > 5 else "MEDIUM" if overrides > 2 else "LOW"
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
def get_days_since_interaction(brain_path: Path) -> int:
|
|
578
|
-
"""Get days since last user interaction (MDR_010)"""
|
|
579
|
-
ledger = load_ledger(brain_path)
|
|
580
|
-
last = ledger.get("last_interaction")
|
|
581
|
-
if not last:
|
|
582
|
-
return -1 # Never interacted
|
|
583
|
-
|
|
584
|
-
last_dt = datetime.fromisoformat(last)
|
|
585
|
-
return (datetime.now() - last_dt).days
|
|
586
|
-
|
|
587
|
-
def check_kill_switch(brain_path: Path) -> Dict:
|
|
588
|
-
"""Check if Kill Switch should activate (MDR_010)"""
|
|
589
|
-
ledger = load_ledger(brain_path)
|
|
590
|
-
days = get_days_since_interaction(brain_path)
|
|
591
|
-
paused = ledger.get("notifications_paused", False)
|
|
592
|
-
|
|
593
|
-
if paused:
|
|
594
|
-
return {"action": "paused", "message": "Notifications already paused by user"}
|
|
595
|
-
|
|
596
|
-
if days >= 14:
|
|
597
|
-
return {
|
|
598
|
-
"action": "escalate",
|
|
599
|
-
"message": f"No interaction for {days} days. Send kill switch prompt.",
|
|
600
|
-
"days_inactive": days
|
|
601
|
-
}
|
|
602
|
-
elif days >= 7:
|
|
603
|
-
return {
|
|
604
|
-
"action": "warn",
|
|
605
|
-
"message": f"Low engagement ({days} days). Consider reducing frequency.",
|
|
606
|
-
"days_inactive": days
|
|
607
|
-
}
|
|
608
|
-
else:
|
|
609
|
-
return {"action": "continue", "days_inactive": days}
|
|
610
|
-
|
|
611
|
-
def pause_notifications(brain_path: Path) -> None:
|
|
612
|
-
"""Pause all notifications (Kill Switch activated)"""
|
|
613
|
-
with get_lock("ledger", brain_path).section():
|
|
614
|
-
ledger = load_ledger(brain_path)
|
|
615
|
-
ledger["notifications_paused"] = True
|
|
616
|
-
save_ledger(brain_path, ledger)
|
|
617
|
-
|
|
618
|
-
def resume_notifications(brain_path: Path) -> None:
|
|
619
|
-
"""Resume notifications after pause"""
|
|
620
|
-
with get_lock("ledger", brain_path).section():
|
|
621
|
-
ledger = load_ledger(brain_path)
|
|
622
|
-
ledger["notifications_paused"] = False
|
|
623
|
-
save_ledger(brain_path, ledger)
|
|
624
|
-
|
|
625
|
-
def get_feedback_path(brain_path: Path) -> Path:
|
|
626
|
-
"""Get path to feedback log"""
|
|
627
|
-
return brain_path / "commitments" / "feedback_log.json"
|
|
628
|
-
|
|
629
|
-
def load_feedback(brain_path: Path) -> List[Dict]:
|
|
630
|
-
"""Load feedback log"""
|
|
631
|
-
path = get_feedback_path(brain_path)
|
|
632
|
-
if brain_file_exists(path):
|
|
633
|
-
return json.loads(read_brain_file(path))
|
|
634
|
-
return []
|
|
635
|
-
|
|
636
|
-
def save_feedback(brain_path: Path, feedback: List[Dict]) -> None:
|
|
637
|
-
"""Save feedback log"""
|
|
638
|
-
path = get_feedback_path(brain_path)
|
|
639
|
-
write_brain_file(path, json.dumps(feedback, indent=2))
|
|
640
|
-
|
|
641
|
-
def record_feedback(
|
|
642
|
-
brain_path: Path,
|
|
643
|
-
notification_type: str,
|
|
644
|
-
score: int,
|
|
645
|
-
response_time_seconds: int = None
|
|
646
|
-
) -> Dict:
|
|
647
|
-
"""Record user feedback on a notification (MDR_010)"""
|
|
648
|
-
# Feedback log also needs locking if simultaneous
|
|
649
|
-
with get_lock("feedback", brain_path).section():
|
|
650
|
-
feedback = load_feedback(brain_path)
|
|
651
|
-
|
|
652
|
-
entry = {
|
|
653
|
-
"timestamp": datetime.now().isoformat(),
|
|
654
|
-
"notification_type": notification_type,
|
|
655
|
-
"score": score, # 1-5 or 0/1 for Y/N
|
|
656
|
-
"response_time_seconds": response_time_seconds
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
feedback.append(entry)
|
|
660
|
-
save_feedback(brain_path, feedback)
|
|
661
|
-
|
|
662
|
-
# Also record interaction
|
|
663
|
-
record_interaction(brain_path)
|
|
664
|
-
|
|
665
|
-
# If positive feedback (4-5 or 1 for Y/N), mark as high impact
|
|
666
|
-
if score >= 4 or score == 1:
|
|
667
|
-
mark_high_impact_closure(brain_path)
|
|
668
|
-
|
|
669
|
-
return entry
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
# ============================================================
|
|
673
|
-
# LIBRARIAN LOGIC (Moved from nightly_agent.py)
|
|
674
|
-
# ============================================================
|
|
675
|
-
|
|
676
|
-
def scan_for_commitments(brain_path: Path) -> Dict:
|
|
677
|
-
"""
|
|
678
|
-
Scan artifacts for new commitments using ripgrep.
|
|
679
|
-
Returns stats of scan.
|
|
680
|
-
"""
|
|
681
|
-
# Scan artifacts folder (was artifacts_path, now root to catch task.md)
|
|
682
|
-
artifacts_path = brain_path
|
|
683
|
-
|
|
684
|
-
# Scan for checklist items
|
|
685
|
-
try:
|
|
686
|
-
result = subprocess.run(
|
|
687
|
-
["rg", "--type", "md", "-n", "--no-heading", "--glob", "!*.resolved.*", "--glob", "!*.resolved", "--glob", "!archive/*", "--glob", "!SPEC_*.md", "--glob", "!NUCLEUS_VISION.md", "--", r"- \[ \]", str(artifacts_path)],
|
|
688
|
-
capture_output=True,
|
|
689
|
-
text=True
|
|
690
|
-
)
|
|
691
|
-
|
|
692
|
-
count = 0
|
|
693
|
-
if result.stdout:
|
|
694
|
-
ledger = load_ledger(brain_path)
|
|
695
|
-
|
|
696
|
-
for line in result.stdout.strip().split('\n'):
|
|
697
|
-
if not line:
|
|
698
|
-
continue
|
|
699
|
-
|
|
700
|
-
parts = line.split(':', 2)
|
|
701
|
-
if len(parts) >= 3:
|
|
702
|
-
file_path = parts[0]
|
|
703
|
-
# Robust parsing for line number
|
|
704
|
-
try:
|
|
705
|
-
line_num = int(parts[1])
|
|
706
|
-
except ValueError:
|
|
707
|
-
continue
|
|
708
|
-
|
|
709
|
-
# Content is the rest
|
|
710
|
-
content = parts[2]
|
|
711
|
-
|
|
712
|
-
description = content.strip().replace('- [ ]', '').strip()
|
|
713
|
-
|
|
714
|
-
# Check if already in ledger
|
|
715
|
-
existing = any(
|
|
716
|
-
c["source_file"] == file_path and
|
|
717
|
-
c["source_line"] == int(line_num) and
|
|
718
|
-
c["status"] == "open"
|
|
719
|
-
for c in ledger["commitments"]
|
|
720
|
-
)
|
|
721
|
-
|
|
722
|
-
if not existing:
|
|
723
|
-
add_commitment(
|
|
724
|
-
brain_path,
|
|
725
|
-
file_path,
|
|
726
|
-
int(line_num),
|
|
727
|
-
description,
|
|
728
|
-
"checklist_item",
|
|
729
|
-
source="scanned"
|
|
730
|
-
)
|
|
731
|
-
count += 1
|
|
732
|
-
|
|
733
|
-
except Exception as e:
|
|
734
|
-
print(f"Checklist scan failed: {e}")
|
|
735
|
-
return {"error": str(e)}
|
|
736
|
-
|
|
737
|
-
# Update ages for all
|
|
738
|
-
update_commitment_ages(brain_path)
|
|
739
|
-
|
|
740
|
-
return {"new_found": count}
|
|
741
|
-
|
|
742
|
-
def auto_archive_stale(brain_path: Path) -> int:
|
|
743
|
-
"""
|
|
744
|
-
Automatically archive commitments older than 30 days.
|
|
745
|
-
Returns count of archived items.
|
|
746
|
-
"""
|
|
747
|
-
ledger = load_ledger(brain_path)
|
|
748
|
-
archived_count = 0
|
|
749
|
-
|
|
750
|
-
for comm in ledger["commitments"]:
|
|
751
|
-
if comm["status"] == "open":
|
|
752
|
-
# Ensure age is set
|
|
753
|
-
created = datetime.fromisoformat(comm["created"])
|
|
754
|
-
age_days = (datetime.now() - created).days
|
|
755
|
-
|
|
756
|
-
if age_days > 30:
|
|
757
|
-
comm["status"] = "closed"
|
|
758
|
-
comm["closed_at"] = datetime.now().isoformat()
|
|
759
|
-
comm["closed_method"] = "auto_archived"
|
|
760
|
-
archived_count += 1
|
|
761
|
-
|
|
762
|
-
if archived_count > 0:
|
|
763
|
-
save_ledger(brain_path, ledger)
|
|
764
|
-
|
|
765
|
-
return archived_count
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
# ============================================================
|
|
769
|
-
# IP STRATEGY (MDR_008)
|
|
770
|
-
# ============================================================
|
|
771
|
-
|
|
772
|
-
def load_brainignore(brain_path: Path) -> List[str]:
|
|
773
|
-
"""Load matching patterns from .brain/.brainignore"""
|
|
774
|
-
ignore_file = brain_path / ".brainignore"
|
|
775
|
-
patterns = []
|
|
776
|
-
|
|
777
|
-
if brain_file_exists(ignore_file):
|
|
778
|
-
for line in read_brain_file(ignore_file).splitlines():
|
|
779
|
-
line = line.strip()
|
|
780
|
-
if line and not line.startswith('#'):
|
|
781
|
-
patterns.append(line)
|
|
782
|
-
|
|
783
|
-
# Add default safety ignores if not present
|
|
784
|
-
defaults = [".DS_Store", "__pycache__", "*.pyc", "*.git*"]
|
|
785
|
-
for d in defaults:
|
|
786
|
-
if d not in patterns:
|
|
787
|
-
patterns.append(d)
|
|
788
|
-
|
|
789
|
-
return patterns
|
|
790
|
-
|
|
791
|
-
def export_brain(brain_path: Path) -> str:
|
|
792
|
-
"""
|
|
793
|
-
Export brain content to a zip file, respecting .brainignore.
|
|
794
|
-
Returns path to export file.
|
|
795
|
-
"""
|
|
796
|
-
import zipfile
|
|
797
|
-
import fnmatch
|
|
798
|
-
import os
|
|
799
|
-
|
|
800
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
801
|
-
export_dir = brain_path / "exports"
|
|
802
|
-
export_dir.mkdir(parents=True, exist_ok=True)
|
|
803
|
-
|
|
804
|
-
export_file = export_dir / f"brain_export_{timestamp}.zip"
|
|
805
|
-
|
|
806
|
-
ignore_patterns = load_brainignore(brain_path)
|
|
807
|
-
# Also ignore the exports directory itself to prevent recursion loops
|
|
808
|
-
ignore_patterns.append("exports/*")
|
|
809
|
-
|
|
810
|
-
file_count = 0
|
|
811
|
-
|
|
812
|
-
with zipfile.ZipFile(export_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
813
|
-
for root, dirs, files in os.walk(brain_path):
|
|
814
|
-
Path(root).relative_to(brain_path)
|
|
815
|
-
|
|
816
|
-
# Skip hidden dirs (like .git) if configured, but allow .brain itself
|
|
817
|
-
# .brainignore handles specific exclusions
|
|
818
|
-
|
|
819
|
-
for file in files:
|
|
820
|
-
file_path = Path(root) / file
|
|
821
|
-
rel_path = file_path.relative_to(brain_path)
|
|
822
|
-
str_path = str(rel_path)
|
|
823
|
-
|
|
824
|
-
# Check ignores
|
|
825
|
-
ignored = False
|
|
826
|
-
for pattern in ignore_patterns:
|
|
827
|
-
if fnmatch.fnmatch(str_path, pattern) or fnmatch.fnmatch(file, pattern):
|
|
828
|
-
ignored = True
|
|
829
|
-
break
|
|
830
|
-
|
|
831
|
-
if not ignored:
|
|
832
|
-
zipf.write(file_path, arcname=str_path)
|
|
833
|
-
file_count += 1
|
|
834
|
-
|
|
835
|
-
return f"Exported {file_count} files to {export_file.name}"
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|