devsquad 3.6.0__py3-none-any.whl
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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import hashlib
|
|
5
|
+
import uuid
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Any, Optional
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from dataclasses import dataclass, field, asdict
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CheckpointStatus(Enum):
|
|
17
|
+
ACTIVE = "active"
|
|
18
|
+
COMPLETED = "completed"
|
|
19
|
+
FAILED = "failed"
|
|
20
|
+
EXPIRED = "expired"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Checkpoint:
|
|
25
|
+
checkpoint_id: str = field(default_factory=lambda: f"cp-{uuid.uuid4().hex[:8]}")
|
|
26
|
+
task_id: str = ""
|
|
27
|
+
step_id: str = ""
|
|
28
|
+
step_name: str = ""
|
|
29
|
+
agent_id: str = ""
|
|
30
|
+
status: CheckpointStatus = CheckpointStatus.ACTIVE
|
|
31
|
+
completed_steps: List[str] = field(default_factory=list)
|
|
32
|
+
remaining_steps: List[str] = field(default_factory=list)
|
|
33
|
+
progress_percentage: float = 0.0
|
|
34
|
+
context_snapshot: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
variables: Dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
outputs: Dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
38
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
39
|
+
expires_at: Optional[str] = None
|
|
40
|
+
checkpoint_hash: str = ""
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
'checkpoint_id': self.checkpoint_id,
|
|
45
|
+
'task_id': self.task_id,
|
|
46
|
+
'step_id': self.step_id,
|
|
47
|
+
'step_name': self.step_name,
|
|
48
|
+
'agent_id': self.agent_id,
|
|
49
|
+
'status': self.status.value if isinstance(self.status, CheckpointStatus) else self.status,
|
|
50
|
+
'completed_steps': self.completed_steps,
|
|
51
|
+
'remaining_steps': self.remaining_steps,
|
|
52
|
+
'progress_percentage': self.progress_percentage,
|
|
53
|
+
'context_snapshot': self.context_snapshot,
|
|
54
|
+
'variables': self.variables,
|
|
55
|
+
'outputs': self.outputs,
|
|
56
|
+
'created_at': self.created_at,
|
|
57
|
+
'updated_at': self.updated_at,
|
|
58
|
+
'expires_at': self.expires_at,
|
|
59
|
+
'checkpoint_hash': self.checkpoint_hash,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'Checkpoint':
|
|
64
|
+
data_copy = dict(data)
|
|
65
|
+
if isinstance(data_copy.get('status'), str):
|
|
66
|
+
try:
|
|
67
|
+
data_copy['status'] = CheckpointStatus(data_copy['status'])
|
|
68
|
+
except ValueError:
|
|
69
|
+
data_copy['status'] = CheckpointStatus.ACTIVE
|
|
70
|
+
return cls(**data_copy)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class HandoffDocument:
|
|
75
|
+
handoff_id: str = field(default_factory=lambda: f"hoff-{uuid.uuid4().hex[:8]}")
|
|
76
|
+
task_id: str = ""
|
|
77
|
+
from_agent: str = ""
|
|
78
|
+
to_agent: str = ""
|
|
79
|
+
completed_work: List[str] = field(default_factory=list)
|
|
80
|
+
current_state: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
next_steps: List[str] = field(default_factory=list)
|
|
82
|
+
pending_issues: List[str] = field(default_factory=list)
|
|
83
|
+
important_notes: List[str] = field(default_factory=list)
|
|
84
|
+
context_for_next: Dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
accumulated_knowledge: Dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
87
|
+
handoff_reason: str = "task_completed"
|
|
88
|
+
confidence: float = 1.0
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
91
|
+
return asdict(self)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'HandoffDocument':
|
|
95
|
+
return cls(**data)
|
|
96
|
+
|
|
97
|
+
def to_markdown(self) -> str:
|
|
98
|
+
md = f"# Task Handoff Document\n\n"
|
|
99
|
+
md += f"## Basic Info\n"
|
|
100
|
+
md += f"- **Handoff ID**: {self.handoff_id}\n"
|
|
101
|
+
md += f"- **Task ID**: {self.task_id}\n"
|
|
102
|
+
md += f"- **Time**: {self.created_at}\n"
|
|
103
|
+
md += f"- **Reason**: {self.handoff_reason}\n"
|
|
104
|
+
md += f"- **Confidence**: {self.confidence:.0%}\n\n"
|
|
105
|
+
md += f"## From -> To\n"
|
|
106
|
+
md += f"- **From**: {self.from_agent}\n"
|
|
107
|
+
md += f"- **To**: {self.to_agent}\n\n---\n\n"
|
|
108
|
+
md += f"## Completed Work\n"
|
|
109
|
+
for i, work in enumerate(self.completed_work, 1):
|
|
110
|
+
md += f"{i}. {work}\n"
|
|
111
|
+
md += f"\n## Current State\n\n```json\n{json.dumps(self.current_state, indent=2, ensure_ascii=False)}\n```\n"
|
|
112
|
+
md += f"\n## Next Steps\n"
|
|
113
|
+
for i, step in enumerate(self.next_steps, 1):
|
|
114
|
+
md += f"{i}. {step}\n"
|
|
115
|
+
if self.pending_issues:
|
|
116
|
+
md += f"\n## Pending Issues\n"
|
|
117
|
+
for i, issue in enumerate(self.pending_issues, 1):
|
|
118
|
+
md += f"{i}. {issue}\n"
|
|
119
|
+
if self.important_notes:
|
|
120
|
+
md += f"\n## Important Notes\n"
|
|
121
|
+
for note in self.important_notes:
|
|
122
|
+
md += f"- {note}\n"
|
|
123
|
+
return md
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CheckpointManager:
|
|
127
|
+
"""
|
|
128
|
+
Checkpoint manager for long-running task state persistence.
|
|
129
|
+
|
|
130
|
+
Features:
|
|
131
|
+
1. Periodic task state saving (like git commits)
|
|
132
|
+
2. Recovery from any checkpoint
|
|
133
|
+
3. Automatic handoff document generation
|
|
134
|
+
4. Data integrity verification (SHA256)
|
|
135
|
+
5. Expired checkpoint auto-cleanup
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, storage_path: str = "./checkpoints"):
|
|
139
|
+
self.storage_path = Path(storage_path)
|
|
140
|
+
self.checkpoints_dir = self.storage_path / "checkpoints"
|
|
141
|
+
self.handoffs_dir = self.storage_path / "handoffs"
|
|
142
|
+
self._file_lock = threading.Lock()
|
|
143
|
+
self._ensure_directories()
|
|
144
|
+
|
|
145
|
+
def _ensure_directories(self):
|
|
146
|
+
self.checkpoints_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
self.handoffs_dir.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
def _compute_hash(self, data: Dict[str, Any]) -> str:
|
|
150
|
+
data_for_hash = {k: v for k, v in data.items() if k != 'checkpoint_hash'}
|
|
151
|
+
json_str = json.dumps(data_for_hash, sort_keys=True, ensure_ascii=False)
|
|
152
|
+
return hashlib.sha256(json_str.encode('utf-8')).hexdigest()
|
|
153
|
+
|
|
154
|
+
def _validate_id(self, id_str: str) -> None:
|
|
155
|
+
if '..' in id_str or '/' in id_str or '\\' in id_str:
|
|
156
|
+
raise ValueError(f"Invalid ID (path traversal detected): {id_str}")
|
|
157
|
+
|
|
158
|
+
def _get_checkpoint_path(self, checkpoint_id: str) -> Path:
|
|
159
|
+
self._validate_id(checkpoint_id)
|
|
160
|
+
path = self.checkpoints_dir / f"{checkpoint_id}.json"
|
|
161
|
+
if not path.resolve().is_relative_to(self.checkpoints_dir.resolve()):
|
|
162
|
+
raise ValueError(f"Path traversal detected: {checkpoint_id}")
|
|
163
|
+
return path
|
|
164
|
+
|
|
165
|
+
def _get_handoff_path(self, handoff_id: str) -> Path:
|
|
166
|
+
self._validate_id(handoff_id)
|
|
167
|
+
path = self.handoffs_dir / f"{handoff_id}.json"
|
|
168
|
+
if not path.resolve().is_relative_to(self.handoffs_dir.resolve()):
|
|
169
|
+
raise ValueError(f"Path traversal detected: {handoff_id}")
|
|
170
|
+
return path
|
|
171
|
+
|
|
172
|
+
def save_checkpoint(self, checkpoint: Checkpoint) -> bool:
|
|
173
|
+
try:
|
|
174
|
+
checkpoint.updated_at = datetime.now().isoformat()
|
|
175
|
+
checkpoint_dict = checkpoint.to_dict()
|
|
176
|
+
checkpoint.checkpoint_hash = self._compute_hash(checkpoint_dict)
|
|
177
|
+
checkpoint_dict['checkpoint_hash'] = checkpoint.checkpoint_hash
|
|
178
|
+
|
|
179
|
+
checkpoint_path = self._get_checkpoint_path(checkpoint.checkpoint_id)
|
|
180
|
+
with self._file_lock:
|
|
181
|
+
with open(checkpoint_path, 'w', encoding='utf-8') as f:
|
|
182
|
+
json.dump(checkpoint_dict, f, indent=2, ensure_ascii=False)
|
|
183
|
+
|
|
184
|
+
logger.info("Checkpoint saved: %s (%.1f%%)", checkpoint.checkpoint_id, checkpoint.progress_percentage)
|
|
185
|
+
return True
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning("Failed to save checkpoint: %s", e)
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def load_checkpoint(self, checkpoint_id: str) -> Optional[Checkpoint]:
|
|
191
|
+
try:
|
|
192
|
+
checkpoint_path = self._get_checkpoint_path(checkpoint_id)
|
|
193
|
+
if not checkpoint_path.exists():
|
|
194
|
+
logger.warning("Checkpoint not found: %s", checkpoint_id)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
with open(checkpoint_path, 'r', encoding='utf-8') as f:
|
|
198
|
+
data = json.load(f)
|
|
199
|
+
|
|
200
|
+
checkpoint = Checkpoint.from_dict(data)
|
|
201
|
+
computed_hash = self._compute_hash({k: v for k, v in data.items() if k != 'checkpoint_hash'})
|
|
202
|
+
if computed_hash != checkpoint.checkpoint_hash:
|
|
203
|
+
logger.warning("Checkpoint integrity check failed: %s", checkpoint_id)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
return checkpoint
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.warning("Failed to load checkpoint: %s", e)
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def get_latest_checkpoint(self, task_id: str) -> Optional[Checkpoint]:
|
|
212
|
+
try:
|
|
213
|
+
task_checkpoints = []
|
|
214
|
+
for cp_path in self.checkpoints_dir.glob("*.json"):
|
|
215
|
+
with open(cp_path, 'r', encoding='utf-8') as f:
|
|
216
|
+
data = json.load(f)
|
|
217
|
+
if data.get('task_id') == task_id:
|
|
218
|
+
task_checkpoints.append((cp_path.stat().st_mtime, Checkpoint.from_dict(data)))
|
|
219
|
+
|
|
220
|
+
if not task_checkpoints:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
latest = sorted(task_checkpoints, key=lambda x: x[0], reverse=True)[0][1]
|
|
224
|
+
return latest
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.warning("Failed to get latest checkpoint: %s", e)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def list_checkpoints(self, task_id: Optional[str] = None) -> List[Checkpoint]:
|
|
230
|
+
try:
|
|
231
|
+
checkpoints = []
|
|
232
|
+
for cp_path in self.checkpoints_dir.glob("*.json"):
|
|
233
|
+
with open(cp_path, 'r', encoding='utf-8') as f:
|
|
234
|
+
data = json.load(f)
|
|
235
|
+
if task_id is None or data.get('task_id') == task_id:
|
|
236
|
+
checkpoints.append(Checkpoint.from_dict(data))
|
|
237
|
+
checkpoints.sort(key=lambda x: x.created_at, reverse=True)
|
|
238
|
+
return checkpoints
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning("Failed to list checkpoints: %s", e)
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
def delete_checkpoint(self, checkpoint_id: str) -> bool:
|
|
244
|
+
try:
|
|
245
|
+
checkpoint_path = self._get_checkpoint_path(checkpoint_id)
|
|
246
|
+
if checkpoint_path.exists():
|
|
247
|
+
checkpoint_path.unlink()
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning("Failed to delete checkpoint: %s", e)
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
def cleanup_expired_checkpoints(self, max_age_hours: int = 24) -> int:
|
|
255
|
+
try:
|
|
256
|
+
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
|
257
|
+
cleaned_count = 0
|
|
258
|
+
for cp_path in self.checkpoints_dir.glob("*.json"):
|
|
259
|
+
if cp_path.stat().st_mtime < cutoff_time.timestamp():
|
|
260
|
+
cp_path.unlink()
|
|
261
|
+
cleaned_count += 1
|
|
262
|
+
if cleaned_count > 0:
|
|
263
|
+
logger.info("Cleaned %d expired checkpoints", cleaned_count)
|
|
264
|
+
return cleaned_count
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.warning("Failed to cleanup expired checkpoints: %s", e)
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
def save_handoff(self, handoff: HandoffDocument) -> bool:
|
|
270
|
+
try:
|
|
271
|
+
handoff_path = self._get_handoff_path(handoff.handoff_id)
|
|
272
|
+
with self._file_lock:
|
|
273
|
+
with open(handoff_path, 'w', encoding='utf-8') as f:
|
|
274
|
+
json.dump(handoff.to_dict(), f, indent=2, ensure_ascii=False)
|
|
275
|
+
|
|
276
|
+
md_path = handoff_path.with_suffix('.md')
|
|
277
|
+
with open(md_path, 'w', encoding='utf-8') as f:
|
|
278
|
+
f.write(handoff.to_markdown())
|
|
279
|
+
|
|
280
|
+
logger.info("Handoff saved: %s -> %s", handoff.from_agent, handoff.to_agent)
|
|
281
|
+
return True
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.warning("Failed to save handoff: %s", e)
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def load_handoff(self, handoff_id: str) -> Optional[HandoffDocument]:
|
|
287
|
+
try:
|
|
288
|
+
handoff_path = self._get_handoff_path(handoff_id)
|
|
289
|
+
if not handoff_path.exists():
|
|
290
|
+
return None
|
|
291
|
+
with open(handoff_path, 'r', encoding='utf-8') as f:
|
|
292
|
+
data = json.load(f)
|
|
293
|
+
return HandoffDocument.from_dict(data)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.warning("Failed to load handoff: %s", e)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def get_task_handoffs(self, task_id: str) -> List[HandoffDocument]:
|
|
299
|
+
try:
|
|
300
|
+
handoffs = []
|
|
301
|
+
for hf_path in self.handoffs_dir.glob("*.json"):
|
|
302
|
+
with open(hf_path, 'r', encoding='utf-8') as f:
|
|
303
|
+
data = json.load(f)
|
|
304
|
+
if data.get('task_id') == task_id:
|
|
305
|
+
handoffs.append(HandoffDocument.from_dict(data))
|
|
306
|
+
handoffs.sort(key=lambda x: x.created_at)
|
|
307
|
+
return handoffs
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.warning("Failed to get task handoffs: %s", e)
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
def create_checkpoint_from_dispatch(
|
|
313
|
+
self,
|
|
314
|
+
task_id: str,
|
|
315
|
+
step_name: str,
|
|
316
|
+
agent_id: str,
|
|
317
|
+
completed_steps: List[str],
|
|
318
|
+
remaining_steps: List[str],
|
|
319
|
+
context: Dict[str, Any] = None,
|
|
320
|
+
outputs: Dict[str, Any] = None,
|
|
321
|
+
) -> Checkpoint:
|
|
322
|
+
total = len(completed_steps) + len(remaining_steps)
|
|
323
|
+
progress = len(completed_steps) / total if total > 0 else 0.0
|
|
324
|
+
|
|
325
|
+
checkpoint = Checkpoint(
|
|
326
|
+
task_id=task_id,
|
|
327
|
+
step_id=f"step-{len(completed_steps)+1}",
|
|
328
|
+
step_name=step_name,
|
|
329
|
+
agent_id=agent_id,
|
|
330
|
+
status=CheckpointStatus.ACTIVE,
|
|
331
|
+
completed_steps=completed_steps,
|
|
332
|
+
remaining_steps=remaining_steps,
|
|
333
|
+
progress_percentage=progress,
|
|
334
|
+
context_snapshot=context or {},
|
|
335
|
+
outputs=outputs or {},
|
|
336
|
+
)
|
|
337
|
+
self.save_checkpoint(checkpoint)
|
|
338
|
+
return checkpoint
|
|
339
|
+
|
|
340
|
+
# ========== Lifecycle State Management (Plan C Integration) ==========
|
|
341
|
+
|
|
342
|
+
def save_lifecycle_state(
|
|
343
|
+
self,
|
|
344
|
+
task_id: str,
|
|
345
|
+
current_phase: Optional[str],
|
|
346
|
+
phase_states: Dict[str, str],
|
|
347
|
+
completed_phases: List[str],
|
|
348
|
+
mode: str = "shortcut",
|
|
349
|
+
gate_results: Dict[str, Dict] = None,
|
|
350
|
+
metadata: Dict[str, Any] = None,
|
|
351
|
+
) -> bool:
|
|
352
|
+
"""
|
|
353
|
+
Save lifecycle state for Plan C unified architecture.
|
|
354
|
+
|
|
355
|
+
Integrates with LifecycleProtocol and UnifiedGateEngine to persist
|
|
356
|
+
lifecycle progress across sessions.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
task_id: Unique task identifier
|
|
360
|
+
current_phase: Current active phase ID (e.g., "P8")
|
|
361
|
+
phase_states: Dict mapping phase_id → state string
|
|
362
|
+
completed_phases: List of completed phase IDs
|
|
363
|
+
mode: Lifecycle mode (shortcut/full/custom)
|
|
364
|
+
gate_results: Optional dict of recent gate check results
|
|
365
|
+
metadata: Additional metadata
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
True if saved successfully
|
|
369
|
+
"""
|
|
370
|
+
try:
|
|
371
|
+
lifecycle_dir = self.storage_path / "lifecycle"
|
|
372
|
+
lifecycle_dir.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
|
|
374
|
+
state_data = {
|
|
375
|
+
"task_id": task_id,
|
|
376
|
+
"current_phase": current_phase,
|
|
377
|
+
"phase_states": phase_states,
|
|
378
|
+
"completed_phases": completed_phases,
|
|
379
|
+
"mode": mode,
|
|
380
|
+
"gate_results": gate_results or {},
|
|
381
|
+
"metadata": metadata or {},
|
|
382
|
+
"saved_at": datetime.now().isoformat(),
|
|
383
|
+
"version": "3.6.0",
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
state_path = lifecycle_dir / f"{task_id}_lifecycle.json"
|
|
387
|
+
with self._file_lock:
|
|
388
|
+
with open(state_path, 'w', encoding='utf-8') as f:
|
|
389
|
+
json.dump(state_data, f, indent=2, ensure_ascii=False)
|
|
390
|
+
|
|
391
|
+
logger.info(
|
|
392
|
+
"Lifecycle state saved: %s (phase=%s, mode=%s)",
|
|
393
|
+
task_id, current_phase, mode,
|
|
394
|
+
)
|
|
395
|
+
return True
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
logger.warning("Failed to save lifecycle state: %s", e)
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
def load_lifecycle_state(self, task_id: str) -> Optional[Dict[str, Any]]:
|
|
402
|
+
"""
|
|
403
|
+
Load lifecycle state for a task.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
task_id: Unique task identifier
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Lifecycle state dict or None if not found
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
lifecycle_dir = self.storage_path / "lifecycle"
|
|
413
|
+
state_path = lifecycle_dir / f"{task_id}_lifecycle.json"
|
|
414
|
+
|
|
415
|
+
if not state_path.exists():
|
|
416
|
+
logger.debug("Lifecycle state not found: %s", task_id)
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
with open(state_path, 'r', encoding='utf-8') as f:
|
|
420
|
+
data = json.load(f)
|
|
421
|
+
|
|
422
|
+
logger.info("Lifecycle state loaded: %s", task_id)
|
|
423
|
+
return data
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.warning("Failed to load lifecycle state: %s", e)
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
def list_lifecycle_states(self) -> List[Dict[str, Any]]:
|
|
430
|
+
"""
|
|
431
|
+
List all saved lifecycle states.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
List of lifecycle state summaries
|
|
435
|
+
"""
|
|
436
|
+
try:
|
|
437
|
+
lifecycle_dir = self.storage_path / "lifecycle"
|
|
438
|
+
if not lifecycle_dir.exists():
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
states = []
|
|
442
|
+
for state_file in lifecycle_dir.glob("*_lifecycle.json"):
|
|
443
|
+
try:
|
|
444
|
+
with open(state_file, 'r', encoding='utf-8') as f:
|
|
445
|
+
data = json.load(f)
|
|
446
|
+
states.append({
|
|
447
|
+
"task_id": data.get("task_id"),
|
|
448
|
+
"current_phase": data.get("current_phase"),
|
|
449
|
+
"mode": data.get("mode"),
|
|
450
|
+
"completed_count": len(data.get("completed_phases", [])),
|
|
451
|
+
"saved_at": data.get("saved_at"),
|
|
452
|
+
})
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.debug("Error reading %s: %e", state_file, e)
|
|
455
|
+
|
|
456
|
+
states.sort(key=lambda x: x.get("saved_at", ""), reverse=True)
|
|
457
|
+
return states
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.warning("Failed to list lifecycle states: %s", e)
|
|
461
|
+
return []
|
|
462
|
+
|
|
463
|
+
def delete_lifecycle_state(self, task_id: str) -> bool:
|
|
464
|
+
"""
|
|
465
|
+
Delete lifecycle state for a task.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
task_id: Unique task identifier
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
True if deleted successfully
|
|
472
|
+
"""
|
|
473
|
+
try:
|
|
474
|
+
lifecycle_dir = self.storage_path / "lifecycle"
|
|
475
|
+
state_path = lifecycle_dir / f"{task_id}_lifecycle.json"
|
|
476
|
+
|
|
477
|
+
if state_path.exists():
|
|
478
|
+
state_path.unlink()
|
|
479
|
+
logger.info("Lifecycle state deleted: %s", task_id)
|
|
480
|
+
return True
|
|
481
|
+
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning("Failed to delete lifecycle state: %s", e)
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
def create_checkpoint_from_lifecycle(
|
|
489
|
+
self,
|
|
490
|
+
task_id: str,
|
|
491
|
+
protocol=None,
|
|
492
|
+
) -> Optional[Checkpoint]:
|
|
493
|
+
"""
|
|
494
|
+
Create a checkpoint from current lifecycle protocol state.
|
|
495
|
+
|
|
496
|
+
This bridges the gap between LifecycleProtocol and CheckpointManager,
|
|
497
|
+
allowing lifecycle state to be persisted as checkpoints.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
task_id: Unique task identifier
|
|
501
|
+
protocol: Optional LifecycleProtocol instance to extract state from
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Created Checkpoint or None on failure
|
|
505
|
+
"""
|
|
506
|
+
try:
|
|
507
|
+
if protocol:
|
|
508
|
+
status = protocol.get_status()
|
|
509
|
+
phase_states = {}
|
|
510
|
+
for phase in protocol.get_all_phases():
|
|
511
|
+
state = protocol._phase_states.get(
|
|
512
|
+
phase.phase_id, "pending"
|
|
513
|
+
)
|
|
514
|
+
phase_states[phase.phase_id] = (
|
|
515
|
+
state.value if hasattr(state, 'value') else str(state)
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Save lifecycle state first
|
|
519
|
+
self.save_lifecycle_state(
|
|
520
|
+
task_id=task_id,
|
|
521
|
+
current_phase=status.current_phase,
|
|
522
|
+
phase_states=phase_states,
|
|
523
|
+
completed_phases=status.completed_phases,
|
|
524
|
+
mode=status.mode.value if hasattr(status.mode, 'value') else str(status.mode),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Create checkpoint
|
|
528
|
+
checkpoint = Checkpoint(
|
|
529
|
+
task_id=task_id,
|
|
530
|
+
step_id=f"phase-{status.current_phase or 'init'}",
|
|
531
|
+
step_name=f"Lifecycle {status.mode.value.upper()}",
|
|
532
|
+
agent_id="lifecycle-protocol",
|
|
533
|
+
status=CheckpointStatus.ACTIVE,
|
|
534
|
+
completed_steps=status.completed_phases,
|
|
535
|
+
remaining_steps=[
|
|
536
|
+
p.phase_id
|
|
537
|
+
for p in protocol.get_all_phases()
|
|
538
|
+
if p.phase_id not in status.completed_phases
|
|
539
|
+
],
|
|
540
|
+
progress_percentage=status.progress_percent,
|
|
541
|
+
context_snapshot={
|
|
542
|
+
"mode": status.mode.value,
|
|
543
|
+
"can_advance": status.can_advance,
|
|
544
|
+
"next_phase": status.next_phase,
|
|
545
|
+
},
|
|
546
|
+
outputs={"lifecycle_status": status.to_summary()},
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
self.save_checkpoint(checkpoint)
|
|
550
|
+
logger.info(
|
|
551
|
+
"Created checkpoint from lifecycle: %s (%.1f%%)",
|
|
552
|
+
checkpoint.checkpoint_id,
|
|
553
|
+
checkpoint.progress_percentage,
|
|
554
|
+
)
|
|
555
|
+
return checkpoint
|
|
556
|
+
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.warning("Failed to create checkpoint from lifecycle: %s", e)
|
|
561
|
+
return None
|