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.
Files changed (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. 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
-