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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. 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