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,1057 +0,0 @@
1
- """
2
- AutopilotEngine - Enterprise-Grade Autonomous Sprint Execution
3
- Multi-slot parallel orchestration with intelligent task assignment.
4
-
5
- Features:
6
- - 4 sprint modes: auto, plan, guided, status
7
- - Tier-matched task assignment with load balancing
8
- - Real-time budget tracking with hard limits
9
- - 7 halt conditions with graceful handling
10
- - Mission-based orchestration with lifecycle
11
- - Checkpoint and recovery support
12
- - <100ms task assignment latency
13
-
14
- Scales: 100+ tasks, 10+ slots, 5 concurrent sprints
15
-
16
- Author: NOP V3.1 - January 2026
17
- """
18
-
19
- import json
20
- import time
21
- import threading
22
- import hashlib
23
- import os
24
- from pathlib import Path
25
- from typing import Dict, List, Optional
26
- from dataclasses import dataclass, field, asdict
27
- from enum import Enum
28
- from datetime import datetime
29
-
30
-
31
- class SprintMode(str, Enum):
32
- """Sprint execution modes."""
33
- AUTO = "auto" # Full autonomous execution
34
- PLAN = "plan" # Dry run, show what would happen
35
- GUIDED = "guided" # Pause for approval at each step
36
- STATUS = "status" # Report current state only
37
-
38
-
39
- class SprintStatus(str, Enum):
40
- """Sprint execution status."""
41
- PENDING = "pending"
42
- RUNNING = "running"
43
- PAUSED = "paused"
44
- COMPLETED = "completed"
45
- PARTIAL = "partial" # Some tasks completed
46
- FAILED = "failed"
47
- HALTED = "halted"
48
-
49
-
50
- class MissionStatus(str, Enum):
51
- """Mission lifecycle status."""
52
- PENDING = "pending"
53
- PLANNING = "planning"
54
- RUNNING = "running"
55
- PAUSED = "paused"
56
- COMPLETED = "completed"
57
- FAILED = "failed"
58
-
59
-
60
- class HaltReason(str, Enum):
61
- """Reasons for sprint halt."""
62
- BUDGET_EXHAUSTED = "budget_exhausted"
63
- ALL_SLOTS_EXHAUSTED = "all_slots_exhausted"
64
- CIRCULAR_DEPENDENCY = "circular_dependency"
65
- CRITICAL_TASK_FAILED = "critical_task_failed"
66
- USER_INTERRUPT = "user_interrupt"
67
- TIME_LIMIT = "time_limit"
68
- TIER_MISMATCH = "tier_mismatch"
69
-
70
-
71
- @dataclass
72
- class BudgetState:
73
- """Budget tracking state."""
74
- limit: float = 0.0
75
- spent: float = 0.0
76
- reserved: float = 0.0
77
- tokens_used: int = 0
78
- reservations: Dict[str, float] = field(default_factory=dict)
79
-
80
- @property
81
- def remaining(self) -> float:
82
- return max(0, self.limit - self.spent - self.reserved)
83
-
84
- @property
85
- def burn_rate(self) -> float:
86
- """Estimate burn rate per hour."""
87
- return 0.0 # Calculated from history
88
-
89
- def can_afford(self, estimated_cost: float) -> bool:
90
- return self.remaining >= estimated_cost
91
-
92
- def reserve(self, task_id: str, amount: float) -> bool:
93
- if not self.can_afford(amount):
94
- return False
95
- self.reservations[task_id] = amount
96
- self.reserved += amount
97
- return True
98
-
99
- def commit(self, task_id: str, actual_cost: float, tokens: int = 0) -> None:
100
- reserved = self.reservations.pop(task_id, 0)
101
- self.reserved -= reserved
102
- self.spent += actual_cost
103
- self.tokens_used += tokens
104
-
105
- def release(self, task_id: str) -> None:
106
- if task_id in self.reservations:
107
- self.reserved -= self.reservations.pop(task_id)
108
-
109
-
110
- @dataclass
111
- class SlotState:
112
- """Slot state for sprint execution."""
113
- slot_id: str
114
- model: str
115
- tier: str
116
- status: str = "idle" # idle, busy, exhausted
117
- current_task: Optional[str] = None
118
- queue_depth: int = 0
119
- tasks_completed: int = 0
120
- tasks_failed: int = 0
121
- last_activity: Optional[str] = None
122
-
123
-
124
- @dataclass
125
- class TaskAssignment:
126
- """Task assignment result."""
127
- task_id: str
128
- slot_id: str
129
- assigned_at: str
130
- estimated_cost: float = 0.0
131
- priority: int = 3
132
-
133
-
134
- @dataclass
135
- class HaltCondition:
136
- """Halt condition with details."""
137
- reason: HaltReason
138
- message: str
139
- recoverable: bool
140
- tasks_affected: List[str] = field(default_factory=list)
141
- timestamp: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
142
- recommendation: str = ""
143
-
144
-
145
- @dataclass
146
- class SprintCheckpoint:
147
- """Checkpoint for sprint recovery."""
148
- sprint_id: str
149
- timestamp: str
150
- wave: int
151
- tasks_completed: List[str]
152
- tasks_in_progress: Dict[str, str] # task_id -> slot_id
153
- tasks_remaining: List[str]
154
- budget_state: Dict
155
- slot_states: List[Dict]
156
-
157
- def to_dict(self) -> Dict:
158
- return asdict(self)
159
-
160
-
161
- @dataclass
162
- class SprintResult:
163
- """Complete sprint execution result."""
164
- sprint_id: str
165
- mission_id: Optional[str]
166
- status: SprintStatus
167
- mode: SprintMode
168
-
169
- # Timing
170
- started_at: str
171
- completed_at: str
172
- duration_seconds: float
173
-
174
- # Task metrics
175
- tasks_total: int
176
- tasks_completed: int
177
- tasks_failed: int
178
- tasks_skipped: int
179
- tasks_remaining: int
180
-
181
- # Slot metrics
182
- slots_used: int
183
- slot_utilization: float
184
- slot_exhaustions: int
185
-
186
- # Budget metrics
187
- budget_limit: Optional[float]
188
- budget_spent: float
189
- tokens_used: int
190
-
191
- # Details
192
- completed_tasks: List[str] = field(default_factory=list)
193
- failed_tasks: List[Dict] = field(default_factory=list)
194
- warnings: List[str] = field(default_factory=list)
195
- halt_reason: Optional[str] = None
196
-
197
- # Recommendations
198
- next_steps: List[str] = field(default_factory=list)
199
-
200
- def to_dict(self) -> Dict:
201
- result = asdict(self)
202
- result["status"] = self.status.value
203
- result["mode"] = self.mode.value
204
- return result
205
-
206
-
207
- @dataclass
208
- class Mission:
209
- """Mission definition for orchestration."""
210
- id: str
211
- name: str
212
- goal: str
213
- success_criteria: List[str]
214
- tasks: List[str]
215
- slots: List[str]
216
- budget_limit: float
217
- time_limit_hours: float
218
- priority: int = 3
219
- status: MissionStatus = MissionStatus.PENDING
220
- created_at: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
221
- started_at: Optional[str] = None
222
- completed_at: Optional[str] = None
223
- result: Optional[Dict] = None
224
-
225
- def to_dict(self) -> Dict:
226
- result = asdict(self)
227
- result["status"] = self.status.value
228
- return result
229
-
230
-
231
- class RetryPolicy:
232
- """Retry policy with exponential backoff."""
233
-
234
- def __init__(self, max_retries: int = 3, backoff_base: float = 2.0, backoff_max: float = 60.0):
235
- self.max_retries = max_retries
236
- self.backoff_base = backoff_base
237
- self.backoff_max = backoff_max
238
- self.attempts: Dict[str, int] = {}
239
-
240
- def get_delay(self, task_id: str) -> float:
241
- attempt = self.attempts.get(task_id, 0)
242
- delay = self.backoff_base ** attempt
243
- return min(delay, self.backoff_max)
244
-
245
- def record_attempt(self, task_id: str) -> int:
246
- self.attempts[task_id] = self.attempts.get(task_id, 0) + 1
247
- return self.attempts[task_id]
248
-
249
- def should_retry(self, task_id: str, error: Exception = None) -> bool:
250
- attempts = self.attempts.get(task_id, 0)
251
- if attempts >= self.max_retries:
252
- return False
253
- # Retry on transient errors
254
- if error:
255
- return isinstance(error, (TimeoutError, ConnectionError))
256
- return True
257
-
258
- def reset(self, task_id: str) -> None:
259
- self.attempts.pop(task_id, None)
260
-
261
-
262
- class WaveAnalyzer:
263
- """Analyze task dependencies and organize into waves."""
264
-
265
- def __init__(self, tasks: List[Dict]):
266
- self.tasks = {t["id"]: t for t in tasks}
267
- self.waves: List[List[str]] = []
268
- self._analyze()
269
-
270
- def _analyze(self) -> None:
271
- """Organize tasks into dependency waves."""
272
- remaining = set(self.tasks.keys())
273
- completed = set()
274
-
275
- while remaining:
276
- # Find tasks with all dependencies satisfied
277
- ready = []
278
- for task_id in remaining:
279
- task = self.tasks[task_id]
280
- deps = set(task.get("blocked_by", []) + task.get("depends_on", []))
281
- if deps <= completed:
282
- ready.append(task_id)
283
-
284
- if not ready:
285
- # Check for circular dependencies
286
- break
287
-
288
- self.waves.append(ready)
289
- completed.update(ready)
290
- remaining -= set(ready)
291
-
292
- # Any remaining are blocked or circular
293
- if remaining:
294
- self.waves.append(list(remaining)) # Put in final wave as blocked
295
-
296
- def get_wave(self, index: int) -> List[str]:
297
- if 0 <= index < len(self.waves):
298
- return self.waves[index]
299
- return []
300
-
301
- def get_wave_count(self) -> int:
302
- return len(self.waves)
303
-
304
- def detect_circular(self) -> List[str]:
305
- """Detect tasks in circular dependencies."""
306
- all_in_waves = set()
307
- for wave in self.waves[:-1]: # Exclude last wave which has blocked
308
- all_in_waves.update(wave)
309
-
310
- circular = []
311
- for task_id, task in self.tasks.items():
312
- if task_id not in all_in_waves:
313
- deps = set(task.get("blocked_by", []) + task.get("depends_on", []))
314
- # Check if any dependency is also not in waves
315
- if any(d not in all_in_waves for d in deps if d in self.tasks):
316
- circular.append(task_id)
317
-
318
- return circular
319
-
320
-
321
- class TaskAssigner:
322
- """Intelligent task assignment with tier matching."""
323
-
324
- TIER_ORDER = ["T1_RESEARCH", "T2_CODE", "T3_REVIEW", "T4_ADMIN"]
325
-
326
- def __init__(self, slots: List[SlotState]):
327
- self.slots = {s.slot_id: s for s in slots}
328
-
329
- def assign(self, task: Dict, force: bool = False) -> Optional[TaskAssignment]:
330
- """Assign task to best available slot."""
331
- required_tier = task.get("required_tier", "T2_CODE")
332
- set(task.get("required_skills", []))
333
-
334
- # Step 1: Filter by tier capability
335
- capable = [s for s in self.slots.values()
336
- if self._tier_capable(s.tier, required_tier) and s.status != "exhausted"]
337
-
338
- if not capable:
339
- if force:
340
- # Force assign to any non-exhausted slot
341
- capable = [s for s in self.slots.values() if s.status != "exhausted"]
342
- if not capable:
343
- return None
344
-
345
- # Step 2: Prefer idle slots
346
- idle = [s for s in capable if s.status == "idle"]
347
- if idle:
348
- # Pick least loaded
349
- best = min(idle, key=lambda s: s.queue_depth)
350
- else:
351
- # Pick slot with shallowest queue
352
- best = min(capable, key=lambda s: s.queue_depth)
353
-
354
- return TaskAssignment(
355
- task_id=task["id"],
356
- slot_id=best.slot_id,
357
- assigned_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
358
- estimated_cost=task.get("estimated_cost", 0.1),
359
- priority=task.get("priority", 3),
360
- )
361
-
362
- def _tier_capable(self, slot_tier: str, required_tier: str) -> bool:
363
- """Check if slot tier can handle required tier."""
364
- try:
365
- slot_idx = self.TIER_ORDER.index(slot_tier) if slot_tier in self.TIER_ORDER else 1
366
- req_idx = self.TIER_ORDER.index(required_tier) if required_tier in self.TIER_ORDER else 1
367
- return slot_idx <= req_idx # Lower index = higher capability
368
- except ValueError:
369
- return True # Default to capable if tier unknown
370
-
371
- def update_slot(self, slot_id: str, **updates) -> None:
372
- """Update slot state."""
373
- if slot_id in self.slots:
374
- for key, value in updates.items():
375
- if hasattr(self.slots[slot_id], key):
376
- setattr(self.slots[slot_id], key, value)
377
-
378
- def get_available_count(self) -> int:
379
- return sum(1 for s in self.slots.values() if s.status != "exhausted")
380
-
381
- def get_idle_count(self) -> int:
382
- return sum(1 for s in self.slots.values() if s.status == "idle")
383
-
384
-
385
- class AutopilotEngine:
386
- """
387
- Enterprise-grade autonomous sprint execution engine.
388
-
389
- Orchestrates multiple slots in parallel with intelligent
390
- task assignment, budget control, and halt conditions.
391
- """
392
-
393
- def __init__(
394
- self,
395
- orchestrator=None,
396
- brain_path: Path = None,
397
- ):
398
- self.orch = orchestrator
399
- self.brain_path = brain_path or Path(os.environ.get("NUCLEAR_BRAIN_PATH", ".brain"))
400
-
401
- # State
402
- self.current_sprint_id: Optional[str] = None
403
- self.current_mission: Optional[Mission] = None
404
- self.budget: BudgetState = BudgetState()
405
- self.retry_policy = RetryPolicy()
406
-
407
- # Tracking
408
- self.completed_tasks: List[str] = []
409
- self.failed_tasks: List[Dict] = []
410
- self.warnings: List[str] = []
411
-
412
- # Threading
413
- self.lock = threading.RLock()
414
- self.halt_requested = False
415
-
416
- def execute_sprint(
417
- self,
418
- slots: List[str] = None,
419
- mode: SprintMode = SprintMode.AUTO,
420
- halt_on_blocker: bool = True,
421
- halt_on_tier_mismatch: bool = False,
422
- max_tasks_per_slot: int = 10,
423
- budget_limit: float = None,
424
- time_limit_hours: float = None,
425
- dry_run: bool = False,
426
- ) -> SprintResult:
427
- """
428
- Execute a sprint with the specified configuration.
429
-
430
- Args:
431
- slots: Slot IDs to use (None = all active)
432
- mode: Execution mode (auto, plan, guided, status)
433
- halt_on_blocker: Stop if circular dependency detected
434
- halt_on_tier_mismatch: Stop if no slot can handle tier
435
- max_tasks_per_slot: Max tasks per slot in one sprint
436
- budget_limit: Max cost in USD (None = unlimited)
437
- time_limit_hours: Max duration (None = unlimited)
438
- dry_run: Override to plan mode
439
-
440
- Returns:
441
- SprintResult with detailed execution report
442
- """
443
- if dry_run:
444
- mode = SprintMode.PLAN
445
-
446
- # Initialize sprint
447
- sprint_id = f"sprint_{int(time.time())}_{hashlib.sha256(str(time.time()).encode()).hexdigest()[:6]}"
448
- self.current_sprint_id = sprint_id
449
- started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
450
- start_time = time.time()
451
-
452
- # Reset state
453
- self.completed_tasks = []
454
- self.failed_tasks = []
455
- self.warnings = []
456
- self.halt_requested = False
457
-
458
- # Initialize budget
459
- if budget_limit:
460
- self.budget = BudgetState(limit=budget_limit)
461
- else:
462
- self.budget = BudgetState(limit=float('inf'))
463
-
464
- # Get tasks and slots
465
- tasks = self._get_pending_tasks()
466
- slot_states = self._get_slot_states(slots)
467
-
468
- if not tasks:
469
- return self._create_result(
470
- sprint_id, started_at, mode, SprintStatus.COMPLETED,
471
- "No pending tasks", tasks, slot_states
472
- )
473
-
474
- if not slot_states:
475
- return self._create_result(
476
- sprint_id, started_at, mode, SprintStatus.FAILED,
477
- "No available slots", tasks, slot_states
478
- )
479
-
480
- # Analyze waves
481
- wave_analyzer = WaveAnalyzer(tasks)
482
- circular = wave_analyzer.detect_circular()
483
-
484
- if circular and halt_on_blocker:
485
- return self._create_result(
486
- sprint_id, started_at, mode, SprintStatus.HALTED,
487
- f"Circular dependencies detected: {circular}", tasks, slot_states
488
- )
489
-
490
- # Status mode - just return current state
491
- if mode == SprintMode.STATUS:
492
- return self._create_status_result(sprint_id, started_at, tasks, slot_states)
493
-
494
- # Plan mode - return execution plan
495
- if mode == SprintMode.PLAN:
496
- return self._create_plan_result(sprint_id, started_at, tasks, slot_states, wave_analyzer)
497
-
498
- # Initialize assigner
499
- assigner = TaskAssigner(slot_states)
500
-
501
- # Execute waves
502
- halt_condition = None
503
- for wave_idx in range(wave_analyzer.get_wave_count()):
504
- wave_tasks = wave_analyzer.get_wave(wave_idx)
505
-
506
- for task_id in wave_tasks:
507
- # Check halt conditions
508
- halt_condition = self._check_halt_conditions(
509
- assigner, time_limit_hours, start_time, halt_on_tier_mismatch
510
- )
511
- if halt_condition or self.halt_requested:
512
- break
513
-
514
- task = next((t for t in tasks if t["id"] == task_id), None)
515
- if not task:
516
- continue
517
-
518
- # Assign task
519
- assignment = assigner.assign(task)
520
- if not assignment:
521
- if halt_on_tier_mismatch:
522
- halt_condition = HaltCondition(
523
- reason=HaltReason.TIER_MISMATCH,
524
- message=f"No slot available for task {task_id}",
525
- recoverable=True,
526
- tasks_affected=[task_id],
527
- )
528
- break
529
- self.warnings.append(f"Could not assign task {task_id}")
530
- continue
531
-
532
- # Reserve budget
533
- if not self.budget.reserve(task_id, assignment.estimated_cost):
534
- halt_condition = HaltCondition(
535
- reason=HaltReason.BUDGET_EXHAUSTED,
536
- message="Budget limit reached",
537
- recoverable=False,
538
- )
539
- break
540
-
541
- # Execute task (in auto/guided mode)
542
- if mode == SprintMode.GUIDED:
543
- # In real implementation, would pause for approval
544
- pass
545
-
546
- success = self._execute_task(task, assignment, assigner)
547
-
548
- if success:
549
- self.completed_tasks.append(task_id)
550
- self.budget.commit(task_id, assignment.estimated_cost, tokens=1000)
551
- else:
552
- if self.retry_policy.should_retry(task_id):
553
- # Re-queue for retry
554
- self.warnings.append(f"Task {task_id} will be retried")
555
- else:
556
- self.failed_tasks.append({
557
- "task_id": task_id,
558
- "error": "Max retries exceeded",
559
- "attempts": self.retry_policy.attempts.get(task_id, 0),
560
- })
561
-
562
- # Check if critical task
563
- if task.get("priority", 3) == 1:
564
- halt_condition = HaltCondition(
565
- reason=HaltReason.CRITICAL_TASK_FAILED,
566
- message=f"Critical task {task_id} failed",
567
- recoverable=False,
568
- tasks_affected=[task_id],
569
- )
570
- break
571
-
572
- if halt_condition or self.halt_requested:
573
- break
574
-
575
- # Checkpoint after each wave
576
- self._save_checkpoint(sprint_id, wave_idx, tasks, assigner)
577
-
578
- # Determine final status
579
- if halt_condition:
580
- status = SprintStatus.HALTED
581
- halt_reason = halt_condition.message
582
- elif self.halt_requested:
583
- status = SprintStatus.HALTED
584
- halt_reason = "User interrupt"
585
- elif len(self.failed_tasks) > 0:
586
- status = SprintStatus.PARTIAL
587
- halt_reason = None
588
- else:
589
- status = SprintStatus.COMPLETED
590
- halt_reason = None
591
-
592
- return self._create_result(
593
- sprint_id, started_at, mode, status, halt_reason, tasks, slot_states
594
- )
595
-
596
- def start_mission(
597
- self,
598
- name: str,
599
- goal: str,
600
- task_ids: List[str],
601
- slot_ids: List[str] = None,
602
- budget_limit: float = 10.0,
603
- time_limit_hours: float = 4.0,
604
- success_criteria: List[str] = None,
605
- ) -> Mission:
606
- """Start a new mission."""
607
- mission_id = f"mission_{int(time.time())}_{hashlib.sha256(name.encode()).hexdigest()[:6]}"
608
-
609
- mission = Mission(
610
- id=mission_id,
611
- name=name,
612
- goal=goal,
613
- success_criteria=success_criteria or [f"Complete all {len(task_ids)} tasks"],
614
- tasks=task_ids,
615
- slots=slot_ids or [],
616
- budget_limit=budget_limit,
617
- time_limit_hours=time_limit_hours,
618
- status=MissionStatus.RUNNING,
619
- started_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
620
- )
621
-
622
- self.current_mission = mission
623
- self._save_mission(mission)
624
-
625
- return mission
626
-
627
- def get_mission_status(self, mission_id: str = None) -> Dict:
628
- """Get current mission status."""
629
- mission = self._load_mission(mission_id) if mission_id else self.current_mission
630
-
631
- if not mission:
632
- return {"error": "No mission found"}
633
-
634
- # Calculate progress
635
- total = len(mission.tasks)
636
- completed = len([t for t in mission.tasks if t in self.completed_tasks])
637
-
638
- return {
639
- "mission_id": mission.id,
640
- "name": mission.name,
641
- "status": mission.status.value,
642
- "progress": {
643
- "total": total,
644
- "completed": completed,
645
- "percent": int(completed / total * 100) if total > 0 else 0,
646
- },
647
- "budget": {
648
- "limit": mission.budget_limit,
649
- "spent": self.budget.spent,
650
- "remaining": mission.budget_limit - self.budget.spent,
651
- },
652
- "started_at": mission.started_at,
653
- "elapsed": self._calculate_elapsed(mission.started_at),
654
- }
655
-
656
- def halt_sprint(self, reason: str = "User requested halt") -> Dict:
657
- """Request halt of current sprint."""
658
- with self.lock:
659
- self.halt_requested = True
660
-
661
- return {
662
- "status": "halt_requested",
663
- "reason": reason,
664
- "sprint_id": self.current_sprint_id,
665
- }
666
-
667
- def resume_sprint(self, sprint_id: str = None) -> SprintResult:
668
- """Resume a halted sprint from checkpoint."""
669
- checkpoint = self._load_checkpoint(sprint_id or self.current_sprint_id)
670
-
671
- if not checkpoint:
672
- return SprintResult(
673
- sprint_id=sprint_id or "unknown",
674
- mission_id=None,
675
- status=SprintStatus.FAILED,
676
- mode=SprintMode.AUTO,
677
- started_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
678
- completed_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
679
- duration_seconds=0,
680
- tasks_total=0,
681
- tasks_completed=0,
682
- tasks_failed=0,
683
- tasks_skipped=0,
684
- tasks_remaining=0,
685
- slots_used=0,
686
- slot_utilization=0,
687
- slot_exhaustions=0,
688
- budget_limit=None,
689
- budget_spent=0,
690
- tokens_used=0,
691
- halt_reason="No checkpoint found",
692
- )
693
-
694
- # Restore state
695
- self.completed_tasks = checkpoint.tasks_completed
696
- self.budget = BudgetState(**checkpoint.budget_state)
697
-
698
- # Continue from checkpoint
699
- return self.execute_sprint(
700
- slots=[s["slot_id"] for s in checkpoint.slot_states],
701
- )
702
-
703
- def _get_pending_tasks(self) -> List[Dict]:
704
- """Get pending tasks from orchestrator."""
705
- if self.orch:
706
- return self.orch.get_all_tasks(status="PENDING")
707
-
708
- # Fallback to file
709
- tasks_path = self.brain_path / "ledger" / "tasks.json"
710
- if tasks_path.exists():
711
- with open(tasks_path) as f:
712
- data = json.load(f)
713
- return [t for t in data.get("tasks", []) if t.get("status") in ["PENDING", "READY"]]
714
-
715
- return []
716
-
717
- def _get_slot_states(self, slot_ids: List[str] = None) -> List[SlotState]:
718
- """Get slot states from registry."""
719
- registry_path = self.brain_path / "slots" / "registry.json"
720
-
721
- if not registry_path.exists():
722
- return []
723
-
724
- with open(registry_path) as f:
725
- registry = json.load(f)
726
-
727
- states = []
728
- for slot_id, slot in registry.get("slots", {}).items():
729
- if slot_ids and slot_id not in slot_ids:
730
- continue
731
- if slot.get("status") == "exhausted":
732
- continue
733
-
734
- states.append(SlotState(
735
- slot_id=slot_id,
736
- model=slot.get("model", "unknown"),
737
- tier=slot.get("tier", "T2_CODE"),
738
- status=slot.get("status", "idle"),
739
- ))
740
-
741
- return states
742
-
743
- def _check_halt_conditions(
744
- self,
745
- assigner: TaskAssigner,
746
- time_limit_hours: float,
747
- start_time: float,
748
- halt_on_tier_mismatch: bool,
749
- ) -> Optional[HaltCondition]:
750
- """Check all halt conditions."""
751
- # Budget exhausted
752
- if self.budget.remaining <= 0:
753
- return HaltCondition(
754
- reason=HaltReason.BUDGET_EXHAUSTED,
755
- message="Budget limit reached",
756
- recoverable=False,
757
- )
758
-
759
- # All slots exhausted
760
- if assigner.get_available_count() == 0:
761
- return HaltCondition(
762
- reason=HaltReason.ALL_SLOTS_EXHAUSTED,
763
- message="All slots exhausted",
764
- recoverable=True,
765
- recommendation="Wait for slot recovery or add new slots",
766
- )
767
-
768
- # Time limit
769
- if time_limit_hours:
770
- elapsed_hours = (time.time() - start_time) / 3600
771
- if elapsed_hours >= time_limit_hours:
772
- return HaltCondition(
773
- reason=HaltReason.TIME_LIMIT,
774
- message=f"Time limit of {time_limit_hours}h exceeded",
775
- recoverable=True,
776
- )
777
-
778
- return None
779
-
780
- def _execute_task(
781
- self,
782
- task: Dict,
783
- assignment: TaskAssignment,
784
- assigner: TaskAssigner,
785
- ) -> bool:
786
- """Execute a single task."""
787
- # Update slot state
788
- assigner.update_slot(
789
- assignment.slot_id,
790
- status="busy",
791
- current_task=task["id"],
792
- )
793
-
794
- # In real implementation, this would delegate to the slot
795
- # For now, simulate execution
796
- success = True # Simulated success
797
-
798
- # Update slot state
799
- if success:
800
- assigner.update_slot(
801
- assignment.slot_id,
802
- status="idle",
803
- current_task=None,
804
- tasks_completed=assigner.slots[assignment.slot_id].tasks_completed + 1,
805
- )
806
- else:
807
- assigner.update_slot(
808
- assignment.slot_id,
809
- status="idle",
810
- current_task=None,
811
- tasks_failed=assigner.slots[assignment.slot_id].tasks_failed + 1,
812
- )
813
-
814
- # Update task in orchestrator
815
- if self.orch:
816
- if success:
817
- self.orch.complete_task(task["id"], assignment.slot_id, "success")
818
- else:
819
- self.orch.update_task(task["id"], {"status": "FAILED"})
820
-
821
- return success
822
-
823
- def _save_checkpoint(
824
- self,
825
- sprint_id: str,
826
- wave: int,
827
- tasks: List[Dict],
828
- assigner: TaskAssigner,
829
- ) -> None:
830
- """Save checkpoint for recovery."""
831
- checkpoint_dir = self.brain_path / "sprints" / sprint_id
832
- checkpoint_dir.mkdir(parents=True, exist_ok=True)
833
-
834
- checkpoint = SprintCheckpoint(
835
- sprint_id=sprint_id,
836
- timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
837
- wave=wave,
838
- tasks_completed=self.completed_tasks.copy(),
839
- tasks_in_progress={s.current_task: s.slot_id for s in assigner.slots.values() if s.current_task},
840
- tasks_remaining=[t["id"] for t in tasks if t["id"] not in self.completed_tasks],
841
- budget_state=asdict(self.budget),
842
- slot_states=[asdict(s) for s in assigner.slots.values()],
843
- )
844
-
845
- with open(checkpoint_dir / "checkpoint.json", "w") as f:
846
- json.dump(checkpoint.to_dict(), f, indent=2)
847
-
848
- def _load_checkpoint(self, sprint_id: str) -> Optional[SprintCheckpoint]:
849
- """Load checkpoint for recovery."""
850
- checkpoint_path = self.brain_path / "sprints" / sprint_id / "checkpoint.json"
851
-
852
- if not checkpoint_path.exists():
853
- return None
854
-
855
- with open(checkpoint_path) as f:
856
- data = json.load(f)
857
-
858
- return SprintCheckpoint(**data)
859
-
860
- def _save_mission(self, mission: Mission) -> None:
861
- """Save mission to disk."""
862
- missions_dir = self.brain_path / "missions"
863
- missions_dir.mkdir(parents=True, exist_ok=True)
864
-
865
- with open(missions_dir / f"{mission.id}.json", "w") as f:
866
- json.dump(mission.to_dict(), f, indent=2)
867
-
868
- def _load_mission(self, mission_id: str) -> Optional[Mission]:
869
- """Load mission from disk."""
870
- mission_path = self.brain_path / "missions" / f"{mission_id}.json"
871
-
872
- if not mission_path.exists():
873
- return None
874
-
875
- with open(mission_path) as f:
876
- data = json.load(f)
877
-
878
- data["status"] = MissionStatus(data["status"])
879
- return Mission(**data)
880
-
881
- def _calculate_elapsed(self, started_at: str) -> str:
882
- """Calculate elapsed time from start."""
883
- try:
884
- start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
885
- now = datetime.utcnow()
886
- elapsed = now - start.replace(tzinfo=None)
887
-
888
- hours = int(elapsed.total_seconds() // 3600)
889
- minutes = int((elapsed.total_seconds() % 3600) // 60)
890
-
891
- return f"{hours}h {minutes}m"
892
- except Exception:
893
- return "N/A"
894
-
895
- def _create_result(
896
- self,
897
- sprint_id: str,
898
- started_at: str,
899
- mode: SprintMode,
900
- status: SprintStatus,
901
- halt_reason: Optional[str],
902
- tasks: List[Dict],
903
- slot_states: List[SlotState],
904
- ) -> SprintResult:
905
- """Create sprint result."""
906
- completed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
907
-
908
- try:
909
- start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
910
- end = datetime.fromisoformat(completed_at.replace("Z", "+00:00"))
911
- duration = (end - start).total_seconds()
912
- except Exception:
913
- duration = 0
914
-
915
- total = len(tasks)
916
- completed = len(self.completed_tasks)
917
- failed = len(self.failed_tasks)
918
-
919
- # Calculate slot metrics
920
- slots_used = len(slot_states)
921
- if slots_used > 0:
922
- active = sum(1 for s in slot_states if s.status != "exhausted")
923
- utilization = active / slots_used
924
- else:
925
- utilization = 0
926
- exhaustions = sum(1 for s in slot_states if s.status == "exhausted")
927
-
928
- return SprintResult(
929
- sprint_id=sprint_id,
930
- mission_id=self.current_mission.id if self.current_mission else None,
931
- status=status,
932
- mode=mode,
933
- started_at=started_at,
934
- completed_at=completed_at,
935
- duration_seconds=duration,
936
- tasks_total=total,
937
- tasks_completed=completed,
938
- tasks_failed=failed,
939
- tasks_skipped=0,
940
- tasks_remaining=total - completed - failed,
941
- slots_used=slots_used,
942
- slot_utilization=utilization,
943
- slot_exhaustions=exhaustions,
944
- budget_limit=self.budget.limit if self.budget.limit != float('inf') else None,
945
- budget_spent=self.budget.spent,
946
- tokens_used=self.budget.tokens_used,
947
- completed_tasks=self.completed_tasks.copy(),
948
- failed_tasks=self.failed_tasks.copy(),
949
- warnings=self.warnings.copy(),
950
- halt_reason=halt_reason,
951
- next_steps=self._generate_next_steps(status, halt_reason),
952
- )
953
-
954
- def _create_status_result(
955
- self,
956
- sprint_id: str,
957
- started_at: str,
958
- tasks: List[Dict],
959
- slot_states: List[SlotState],
960
- ) -> SprintResult:
961
- """Create status-only result."""
962
- return self._create_result(
963
- sprint_id, started_at, SprintMode.STATUS, SprintStatus.PENDING,
964
- None, tasks, slot_states
965
- )
966
-
967
- def _create_plan_result(
968
- self,
969
- sprint_id: str,
970
- started_at: str,
971
- tasks: List[Dict],
972
- slot_states: List[SlotState],
973
- wave_analyzer: WaveAnalyzer,
974
- ) -> SprintResult:
975
- """Create plan-only result."""
976
- result = self._create_result(
977
- sprint_id, started_at, SprintMode.PLAN, SprintStatus.PENDING,
978
- None, tasks, slot_states
979
- )
980
-
981
- # Add wave information to next_steps
982
- result.next_steps = [
983
- f"Wave 1: {len(wave_analyzer.get_wave(0))} tasks (roots)",
984
- ]
985
- for i in range(1, wave_analyzer.get_wave_count()):
986
- result.next_steps.append(
987
- f"Wave {i+1}: {len(wave_analyzer.get_wave(i))} tasks"
988
- )
989
-
990
- return result
991
-
992
- def _generate_next_steps(self, status: SprintStatus, halt_reason: Optional[str]) -> List[str]:
993
- """Generate recommended next steps."""
994
- steps = []
995
-
996
- if status == SprintStatus.COMPLETED:
997
- steps.append("All tasks completed successfully")
998
- steps.append("Review completed work and verify quality")
999
-
1000
- elif status == SprintStatus.PARTIAL:
1001
- steps.append(f"Retry failed tasks: {len(self.failed_tasks)}")
1002
- steps.append("Review failure logs for root cause")
1003
-
1004
- elif status == SprintStatus.HALTED:
1005
- if "budget" in (halt_reason or "").lower():
1006
- steps.append("Increase budget limit and resume")
1007
- elif "slot" in (halt_reason or "").lower():
1008
- steps.append("Wait for slot recovery or spawn new slots")
1009
- elif "circular" in (halt_reason or "").lower():
1010
- steps.append("Break circular dependencies manually")
1011
- else:
1012
- steps.append("Review halt reason and resume when ready")
1013
-
1014
- return steps
1015
-
1016
-
1017
- def format_sprint_result(result: SprintResult) -> str:
1018
- """Format sprint result as ASCII report."""
1019
- lines = [
1020
- f"🚀 Sprint Report: {result.sprint_id}",
1021
- "═" * 50,
1022
- f"Status: {result.status.value.upper()}",
1023
- f"Mode: {result.mode.value}",
1024
- f"Duration: {result.duration_seconds:.1f}s",
1025
- "",
1026
- "📊 TASKS",
1027
- f" ├── Total: {result.tasks_total}",
1028
- f" ├── Completed: {result.tasks_completed}",
1029
- f" ├── Failed: {result.tasks_failed}",
1030
- f" └── Remaining: {result.tasks_remaining}",
1031
- "",
1032
- "🤖 SLOTS",
1033
- f" ├── Used: {result.slots_used}",
1034
- f" ├── Utilization: {int(result.slot_utilization * 100)}%",
1035
- f" └── Exhaustions: {result.slot_exhaustions}",
1036
- "",
1037
- "💰 BUDGET",
1038
- f" ├── Spent: ${result.budget_spent:.2f}",
1039
- f" └── Tokens: {result.tokens_used}",
1040
- ]
1041
-
1042
- if result.halt_reason:
1043
- lines.extend([
1044
- "",
1045
- "⚠️ HALT REASON",
1046
- f" {result.halt_reason}",
1047
- ])
1048
-
1049
- if result.next_steps:
1050
- lines.extend([
1051
- "",
1052
- "📋 NEXT STEPS",
1053
- ])
1054
- for step in result.next_steps:
1055
- lines.append(f" • {step}")
1056
-
1057
- return "\n".join(lines)