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,617 +0,0 @@
1
- """
2
- NOP V3.1 Unified Orchestrator
3
-
4
- Converges Track A (Pure Python Core) with Track B (MCP Integration):
5
- - CRDTTaskStore: 15K+ writes/sec, LWW + vector clocks
6
- - TaskScheduler: 423K tasks/sec, intelligent routing
7
- - V3.1 Features: Reset cycles, checkpoints, context summaries
8
-
9
- This is the CORE orchestration engine that MCP tools delegate to.
10
- """
11
-
12
- import json
13
- import time
14
- import os
15
- from pathlib import Path
16
- from typing import Dict, List, Optional, Any
17
- from datetime import datetime
18
- from dataclasses import dataclass, field
19
-
20
- from .crdt_task_store import CRDTTaskStore
21
- from .task_scheduler import TaskScheduler
22
-
23
- # V3.1 Components - lazy loaded to avoid circular imports
24
- _agent_pool = None
25
- _ingestion_engine = None
26
-
27
-
28
- def _get_agent_pool():
29
- """Lazy load AgentPool to avoid circular imports."""
30
- global _agent_pool
31
- if _agent_pool is None:
32
- try:
33
- from .agent_pool import AgentPool
34
- _agent_pool = AgentPool()
35
- except ImportError:
36
- pass
37
- return _agent_pool
38
-
39
-
40
- def _get_ingestion_engine(brain_path=None):
41
- """Lazy load TaskIngestionEngine."""
42
- global _ingestion_engine
43
- if _ingestion_engine is None:
44
- try:
45
- from .task_ingestion import TaskIngestionEngine
46
- _ingestion_engine = TaskIngestionEngine(brain_path=brain_path)
47
- except ImportError:
48
- pass
49
- return _ingestion_engine
50
-
51
-
52
- def get_brain_path() -> Path:
53
- """Get brain path from environment or default."""
54
- return Path(os.getenv("NUCLEAR_BRAIN_PATH", "/Users/lokeshgarg/ai-mvp-backend/.brain"))
55
-
56
-
57
- @dataclass
58
- class ResetCycleInfo:
59
- """V3.1: Reset cycle configuration for model-specific limits."""
60
- hours: Optional[int] = None # None = unlimited (Opus 4.5)
61
- last_reset_at: Optional[str] = None
62
- next_reset_at: Optional[str] = None
63
- warning_threshold_minutes: int = 30
64
-
65
-
66
- @dataclass
67
- class Checkpoint:
68
- """V3.1: Checkpoint for long-running tasks."""
69
- enabled: bool = False
70
- last_checkpoint_at: Optional[str] = None
71
- data: Dict[str, Any] = field(default_factory=dict)
72
-
73
-
74
- @dataclass
75
- class ContextSummary:
76
- """V3.1: Context summary for handoffs."""
77
- generated_at: Optional[str] = None
78
- summary: str = ""
79
- key_decisions: List[str] = field(default_factory=list)
80
- handoff_notes: str = ""
81
-
82
-
83
- class NucleusOrchestratorV3:
84
- """
85
- Unified orchestration engine for NOP V3.1.
86
-
87
- Combines:
88
- - CRDTTaskStore (conflict-free task storage)
89
- - TaskScheduler (intelligent task assignment)
90
- - V3.1 features (checkpoints, context summaries, reset cycles)
91
-
92
- Provides:
93
- - All internal _impl functions for MCP tools
94
- - Backward compatibility with V3.0 tools
95
- - High-performance operations from Track A
96
- """
97
-
98
- def __init__(self, brain_path: Optional[Path] = None):
99
- self.brain_path = brain_path or get_brain_path()
100
- self.replica_id = f"nucleus_{int(time.time())}"
101
-
102
- # Initialize Track A components
103
- self.task_store = CRDTTaskStore(replica_id=self.replica_id)
104
- self.scheduler = TaskScheduler(max_agents=1000)
105
-
106
- # Load existing state from JSON files (backward compat)
107
- self._load_from_legacy_json()
108
-
109
- def _load_from_legacy_json(self):
110
- """Load existing tasks from V3.0/V3.1 JSON files."""
111
- tasks_path = self.brain_path / "ledger" / "tasks.json"
112
- if tasks_path.exists():
113
- try:
114
- with open(tasks_path) as f:
115
- data = json.load(f)
116
-
117
- for task in data.get("tasks", []):
118
- # Map to CRDT format
119
- crdt_task = {
120
- "id": task.get("id"),
121
- "title": task.get("description", task.get("title", "")),
122
- "status": task.get("status", "PENDING"),
123
- "tier": task.get("required_tier", "T2_CODE"),
124
- "priority": task.get("priority", 3),
125
- "claimed_by": task.get("claimed_by"),
126
- "blocked_by": task.get("blocked_by", []),
127
- # V3.1 fields
128
- "checkpoint": task.get("checkpoint"),
129
- "context_summary": task.get("context_summary"),
130
- "dependency_metadata": task.get("dependency_metadata", {})
131
- }
132
- self.task_store.add_task(crdt_task)
133
- except Exception as e:
134
- print(f"Warning: Failed to load legacy tasks: {e}")
135
-
136
- def _save_to_legacy_json(self):
137
- """Save CRDT state back to legacy JSON format."""
138
- tasks_path = self.brain_path / "ledger" / "tasks.json"
139
-
140
- # Load existing to preserve metadata
141
- existing = {"tasks": [], "metadata": {"version": "3.1"}}
142
- if tasks_path.exists():
143
- with open(tasks_path) as f:
144
- existing = json.load(f)
145
-
146
- # Convert CRDT tasks to legacy format
147
- crdt_tasks = self.task_store.get_all_tasks()
148
- legacy_tasks = []
149
-
150
- for task in crdt_tasks:
151
- legacy_task = {
152
- "id": task["id"],
153
- "description": task.get("title", ""),
154
- "status": task.get("status", "PENDING"),
155
- "priority": task.get("priority", 3),
156
- "required_tier": task.get("tier", "standard"),
157
- "claimed_by": task.get("claimed_by"),
158
- "blocked_by": task.get("blocked_by", []),
159
- "checkpoint": task.get("checkpoint"),
160
- "context_summary": task.get("context_summary"),
161
- "dependency_metadata": task.get("dependency_metadata", {})
162
- }
163
- legacy_tasks.append(legacy_task)
164
-
165
- existing["tasks"] = legacy_tasks
166
- existing["metadata"]["last_synced"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
167
-
168
- with open(tasks_path, "w") as f:
169
- json.dump(existing, f, indent=2)
170
-
171
- # =========================================================================
172
- # CORE OPERATIONS
173
- # =========================================================================
174
-
175
- def get_task(self, task_id: str) -> Optional[Dict]:
176
- """Get a single task by ID."""
177
- return self.task_store.get_task(task_id)
178
-
179
- def get_all_tasks(self, status: Optional[str] = None) -> List[Dict]:
180
- """Get all tasks, optionally filtered by status."""
181
- tasks = self.task_store.get_all_tasks()
182
- if status:
183
- tasks = [t for t in tasks if t.get("status") == status]
184
- return tasks
185
-
186
- def claim_task(self, task_id: str, agent_id: str) -> Dict:
187
- """Atomic task claiming with conflict detection."""
188
- task = self.task_store.get_task(task_id)
189
- if not task:
190
- return {"success": False, "error": f"Task {task_id} not found"}
191
-
192
- if task.get("claimed_by") and task["claimed_by"] != agent_id:
193
- return {
194
- "success": False,
195
- "error": f"Task already claimed by {task['claimed_by']}"
196
- }
197
-
198
- # Update task with claim
199
- task["claimed_by"] = agent_id
200
- task["status"] = "IN_PROGRESS"
201
- task["claimed_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
202
-
203
- self.task_store.update_task(task_id, task)
204
- self._save_to_legacy_json()
205
-
206
- return {"success": True, "task": task}
207
-
208
- def complete_task(self, task_id: str, agent_id: str, outcome: str = "success") -> Dict:
209
- """Mark task as complete."""
210
- task = self.task_store.get_task(task_id)
211
- if not task:
212
- return {"success": False, "error": f"Task {task_id} not found"}
213
-
214
- task["status"] = "DONE" if outcome == "success" else "FAILED"
215
- task["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
216
- task["completed_by"] = agent_id
217
-
218
- self.task_store.update_task(task_id, task)
219
- self._save_to_legacy_json()
220
-
221
- return {"success": True, "task": task}
222
-
223
- def add_task(self, description: str, priority: int = 3,
224
- tier: str = "T2_CODE", blocked_by: List[str] = None,
225
- source: str = "user", required_skills: List[str] = None) -> Dict:
226
- """Create new task via orchestrator."""
227
- task_id = f"task_{int(time.time() * 1000)}"
228
- task = {
229
- "id": task_id,
230
- "title": description,
231
- "description": description,
232
- "status": "PENDING",
233
- "tier": tier,
234
- "priority": priority,
235
- "blocked_by": blocked_by or [],
236
- "required_skills": required_skills or [],
237
- "source": source,
238
- "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
239
- # V3.1 fields
240
- "checkpoint": None,
241
- "context_summary": None,
242
- "dependency_metadata": {"depth": 0, "blocks": []}
243
- }
244
- self.task_store.add_task(task)
245
- self._save_to_legacy_json()
246
- return {"success": True, "task": task}
247
-
248
- def update_task(self, task_id: str, updates: Dict) -> Dict:
249
- """Update task fields."""
250
- task = self.task_store.get_task(task_id)
251
- if not task:
252
- return {"success": False, "error": f"Task {task_id} not found"}
253
-
254
- # Apply updates (never update ID)
255
- for key, value in updates.items():
256
- if key != "id":
257
- task[key] = value
258
- task["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
259
-
260
- self.task_store.update_task(task_id, task)
261
- self._save_to_legacy_json()
262
- return {"success": True, "task": task}
263
-
264
- def get_next_task(self, skills: List[str] = None,
265
- agent_tier: str = None) -> Optional[Dict]:
266
- """Get highest priority unblocked task."""
267
- tasks = self.task_store.get_all_tasks()
268
-
269
- # Filter to pending/ready tasks
270
- available = [t for t in tasks if t.get("status") in ["PENDING", "READY"]]
271
-
272
- # Filter out blocked tasks
273
- done_ids = {t["id"] for t in tasks if t.get("status") == "DONE"}
274
- unblocked = []
275
- for t in available:
276
- deps = t.get("blocked_by", []) + t.get("depends_on", [])
277
- if all(d in done_ids for d in deps):
278
- unblocked.append(t)
279
-
280
- if not unblocked:
281
- return None
282
-
283
- # Sort by priority (lower = higher priority)
284
- unblocked.sort(key=lambda x: x.get("priority", 3))
285
- return unblocked[0]
286
-
287
- def resume_from_checkpoint(self, task_id: str) -> Dict:
288
- """Get checkpoint data for task resumption."""
289
- task = self.task_store.get_task(task_id)
290
- if not task:
291
- return {"success": False, "error": f"Task {task_id} not found"}
292
-
293
- checkpoint = task.get("checkpoint")
294
- if not checkpoint:
295
- return {"success": False, "error": "No checkpoint found for task"}
296
-
297
- return {
298
- "success": True,
299
- "task_id": task_id,
300
- "checkpoint": checkpoint,
301
- "context_summary": task.get("context_summary"),
302
- "resume_instructions": f"Resume from step {checkpoint.get('data', {}).get('step', 'unknown')}"
303
- }
304
-
305
- def escalate_task(self, task_id: str, reason: str) -> Dict:
306
- """Mark task as needing human intervention."""
307
- task = self.task_store.get_task(task_id)
308
- if not task:
309
- return {"success": False, "error": f"Task {task_id} not found"}
310
-
311
- task["status"] = "ESCALATED"
312
- task["escalation"] = {
313
- "reason": reason,
314
- "escalated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
315
- "resolved": False
316
- }
317
-
318
- self.task_store.update_task(task_id, task)
319
- self._save_to_legacy_json()
320
- return {"success": True, "task": task}
321
-
322
- # =========================================================================
323
- # V3.1 FEATURES
324
- # =========================================================================
325
-
326
- def checkpoint_task(self, task_id: str, checkpoint_data: Dict) -> Dict:
327
- """Save checkpoint for long-running task."""
328
- task = self.task_store.get_task(task_id)
329
- if not task:
330
- return {"success": False, "error": f"Task {task_id} not found"}
331
-
332
- task["checkpoint"] = {
333
- "enabled": True,
334
- "last_checkpoint_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
335
- "data": checkpoint_data
336
- }
337
-
338
- self.task_store.update_task(task_id, task)
339
- self._save_to_legacy_json()
340
-
341
- return {"success": True, "checkpoint": task["checkpoint"]}
342
-
343
- def generate_context_summary(self, task_id: str, summary: str,
344
- key_decisions: List[str] = None,
345
- handoff_notes: str = "") -> Dict:
346
- """Generate handoff context summary for a task."""
347
- task = self.task_store.get_task(task_id)
348
- if not task:
349
- return {"success": False, "error": f"Task {task_id} not found"}
350
-
351
- task["context_summary"] = {
352
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
353
- "summary": summary,
354
- "key_decisions": key_decisions or [],
355
- "handoff_notes": handoff_notes
356
- }
357
-
358
- self.task_store.update_task(task_id, task)
359
- self._save_to_legacy_json()
360
-
361
- return {"success": True, "context_summary": task["context_summary"]}
362
-
363
- def check_reset_warnings(self) -> List[Dict]:
364
- """Check all slots for approaching resets."""
365
- warnings = []
366
- registry_path = self.brain_path / "slots" / "registry.json"
367
-
368
- if not registry_path.exists():
369
- return warnings
370
-
371
- with open(registry_path) as f:
372
- registry = json.load(f)
373
-
374
- now = datetime.now()
375
-
376
- for slot_id, slot in registry.get("slots", {}).items():
377
- reset_cycle = slot.get("reset_cycle")
378
- if not reset_cycle or not reset_cycle.get("next_reset_at"):
379
- continue
380
-
381
- try:
382
- next_reset = datetime.fromisoformat(reset_cycle["next_reset_at"].replace("Z", "+00:00"))
383
- time_remaining = (next_reset - now).total_seconds() / 60
384
- threshold = reset_cycle.get("warning_threshold_minutes", 30)
385
-
386
- if time_remaining <= threshold and time_remaining > 0:
387
- warnings.append({
388
- "slot_id": slot_id,
389
- "minutes_remaining": int(time_remaining),
390
- "next_reset_at": reset_cycle["next_reset_at"],
391
- "action": "PREPARE_HANDOFF"
392
- })
393
- except (ValueError, TypeError):
394
- continue
395
-
396
- return warnings
397
-
398
- def record_exhaustion(self, slot_id: str, reason: str,
399
- tasks_affected: List[str] = None) -> Dict:
400
- """Record slot exhaustion event."""
401
- registry_path = self.brain_path / "slots" / "registry.json"
402
-
403
- if not registry_path.exists():
404
- return {"success": False, "error": "Registry not found"}
405
-
406
- with open(registry_path) as f:
407
- registry = json.load(f)
408
-
409
- slot = registry.get("slots", {}).get(slot_id)
410
- if not slot:
411
- return {"success": False, "error": f"Slot {slot_id} not found"}
412
-
413
- # Initialize exhaustion_history if needed
414
- if "exhaustion_history" not in slot:
415
- slot["exhaustion_history"] = []
416
-
417
- # Add exhaustion event
418
- event = {
419
- "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
420
- "reason": reason,
421
- "tasks_affected": tasks_affected or [],
422
- "recovery_time_seconds": 300 if reason == "rate_limit_hit" else 18000
423
- }
424
- slot["exhaustion_history"].append(event)
425
-
426
- # Keep only last 10 events
427
- slot["exhaustion_history"] = slot["exhaustion_history"][-10:]
428
-
429
- registry["slots"][slot_id] = slot
430
-
431
- with open(registry_path, "w") as f:
432
- json.dump(registry, f, indent=2)
433
-
434
- return {"success": True, "event": event}
435
-
436
- # =========================================================================
437
- # AGENT POOL OPERATIONS (V3.1)
438
- # =========================================================================
439
-
440
- def get_agent_pool(self):
441
- """Get the AgentPool instance."""
442
- return _get_agent_pool()
443
-
444
- def spawn_agent(self, model: str, tier: str = "T2_CODE",
445
- alias: str = None) -> Dict:
446
- """Spawn a new agent in the pool."""
447
- pool = _get_agent_pool()
448
- if pool is None:
449
- return {"success": False, "error": "AgentPool not available"}
450
- return pool.spawn_agent(model=model, tier=tier, alias=alias)
451
-
452
- def get_agent(self, agent_id: str) -> Optional[Dict]:
453
- """Get agent by ID."""
454
- pool = _get_agent_pool()
455
- if pool is None:
456
- return None
457
- return pool.get_agent(agent_id)
458
-
459
- def mark_agent_exhausted(self, agent_id: str, reason: str = "rate_limit") -> Dict:
460
- """Mark an agent as exhausted."""
461
- pool = _get_agent_pool()
462
- if pool is None:
463
- return {"success": False, "error": "AgentPool not available"}
464
- return pool.exhaust_agent(agent_id, reason=reason)
465
-
466
- def respawn_agent(self, agent_id: str) -> Dict:
467
- """Respawn an exhausted agent."""
468
- pool = _get_agent_pool()
469
- if pool is None:
470
- return {"success": False, "error": "AgentPool not available"}
471
- return pool.respawn_agent(agent_id)
472
-
473
- def get_available_agent(self, tier: str = None) -> Optional[str]:
474
- """Get an available agent for the given tier."""
475
- pool = _get_agent_pool()
476
- if pool is None:
477
- return None
478
- return pool.get_available_agent(tier=tier)
479
-
480
- def assign_task_to_agent(self, task_id: str, agent_id: str = None,
481
- tier: str = None) -> Dict:
482
- """Assign a task to an agent (or find best available)."""
483
- pool = _get_agent_pool()
484
- if pool is None:
485
- return {"success": False, "error": "AgentPool not available"}
486
-
487
- if agent_id is None and tier:
488
- agent_id = pool.get_available_agent(tier=tier)
489
- if agent_id is None:
490
- return {"success": False, "error": f"No available agent for tier {tier}"}
491
-
492
- return pool.assign_task(task_id, agent_id=agent_id)
493
-
494
- # =========================================================================
495
- # TASK INGESTION OPERATIONS (V3.1)
496
- # =========================================================================
497
-
498
- def get_ingestion_engine(self):
499
- """Get the TaskIngestionEngine instance."""
500
- return _get_ingestion_engine(self.brain_path)
501
-
502
- def ingest_tasks(self, source: str, source_type: str = "auto",
503
- session_id: str = None, auto_assign: bool = False,
504
- skip_dedup: bool = False, dry_run: bool = False) -> Dict:
505
- """Ingest tasks from various sources."""
506
- engine = _get_ingestion_engine(self.brain_path)
507
- if engine is None:
508
- return {"success": False, "error": "TaskIngestionEngine not available"}
509
-
510
- import os
511
- if os.path.exists(source):
512
- result = engine.ingest_from_file(
513
- source, source_type=source_type, session_id=session_id,
514
- auto_assign=auto_assign, skip_dedup=skip_dedup, dry_run=dry_run
515
- )
516
- else:
517
- result = engine.ingest_from_text(
518
- source, source_type=source_type if source_type != "auto" else "manual",
519
- session_id=session_id, auto_assign=auto_assign,
520
- skip_dedup=skip_dedup, dry_run=dry_run
521
- )
522
-
523
- return result.to_dict()
524
-
525
- def rollback_ingestion(self, batch_id: str, reason: str = None) -> Dict:
526
- """Rollback an ingestion batch."""
527
- engine = _get_ingestion_engine(self.brain_path)
528
- if engine is None:
529
- return {"success": False, "error": "TaskIngestionEngine not available"}
530
- return engine.rollback(batch_id, reason)
531
-
532
- def get_ingestion_stats(self) -> Dict:
533
- """Get ingestion statistics."""
534
- engine = _get_ingestion_engine(self.brain_path)
535
- if engine is None:
536
- return {"error": "TaskIngestionEngine not available"}
537
- return engine.get_ingestion_stats()
538
-
539
- # =========================================================================
540
- # METRICS & MONITORING
541
- # =========================================================================
542
-
543
- def get_pool_metrics(self) -> Dict:
544
- """Get pool-wide metrics."""
545
- tasks = self.task_store.get_all_tasks()
546
-
547
- return {
548
- "total_tasks": len(tasks),
549
- "pending": len([t for t in tasks if t.get("status") in ["PENDING", "READY"]]),
550
- "in_progress": len([t for t in tasks if t.get("status") == "IN_PROGRESS"]),
551
- "blocked": len([t for t in tasks if t.get("status") == "BLOCKED"]),
552
- "done": len([t for t in tasks if t.get("status") == "DONE"]),
553
- "failed": len([t for t in tasks if t.get("status") == "FAILED"]),
554
- "with_checkpoints": len([t for t in tasks if t.get("checkpoint")]),
555
- "with_summaries": len([t for t in tasks if t.get("context_summary")])
556
- }
557
-
558
- def get_dependency_graph(self) -> Dict:
559
- """Compute dependency graph for all tasks."""
560
- tasks = self.task_store.get_all_tasks()
561
-
562
- # Build forward dependencies (task -> what it depends on)
563
- forward = {}
564
- # Build reverse dependencies (task -> what depends on it)
565
- reverse = {}
566
-
567
- for task in tasks:
568
- task_id = task["id"]
569
- deps = task.get("blocked_by", []) + task.get("depends_on", [])
570
- forward[task_id] = deps
571
-
572
- for dep in deps:
573
- if dep not in reverse:
574
- reverse[dep] = []
575
- reverse[dep].append(task_id)
576
-
577
- # Compute depths
578
- depths = {}
579
-
580
- def compute_depth(task_id: str, visited: set) -> int:
581
- if task_id in depths:
582
- return depths[task_id]
583
- if task_id in visited:
584
- return -1 # Cycle detected
585
-
586
- visited.add(task_id)
587
- deps = forward.get(task_id, [])
588
-
589
- if not deps:
590
- depths[task_id] = 0
591
- return 0
592
-
593
- max_dep_depth = max(compute_depth(d, visited) for d in deps)
594
- depths[task_id] = max_dep_depth + 1 if max_dep_depth >= 0 else -1
595
- return depths[task_id]
596
-
597
- for task in tasks:
598
- compute_depth(task["id"], set())
599
-
600
- return {
601
- "forward_deps": forward,
602
- "reverse_deps": reverse,
603
- "depths": depths,
604
- "computed_at": time.strftime("%Y-%m-%dT%H:%M:%S%z")
605
- }
606
-
607
-
608
- # Singleton instance
609
- _orchestrator: Optional[NucleusOrchestratorV3] = None
610
-
611
-
612
- def get_orchestrator() -> NucleusOrchestratorV3:
613
- """Get or create the singleton orchestrator instance."""
614
- global _orchestrator
615
- if _orchestrator is None:
616
- _orchestrator = NucleusOrchestratorV3()
617
- return _orchestrator