nucleus-mcp 1.0.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- package/tests/test_search.py +0 -69
|
@@ -1,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)
|