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,1446 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Unified Lifecycle Architecture (Plan C Implementation)
|
|
5
|
+
|
|
6
|
+
Core abstractions:
|
|
7
|
+
- LifecycleMode: SHORTCUT / FULL / CUSTOM enum
|
|
8
|
+
- PhaseDefinition: Unified phase structure
|
|
9
|
+
- ViewMapping: CLI command → 11-phase mapping
|
|
10
|
+
- LifecycleProtocol: Abstract interface for lifecycle management
|
|
11
|
+
|
|
12
|
+
Spec reference: docs/spec/SPEC_Lifecycle_Unified_Architecture_C.md
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LifecycleMode(Enum):
|
|
26
|
+
"""
|
|
27
|
+
Three lifecycle modes for different usage scenarios.
|
|
28
|
+
|
|
29
|
+
Modes:
|
|
30
|
+
- SHORTCUT: CLI 6-command simplified view (spec/plan/build/test/review/ship)
|
|
31
|
+
- FULL: Complete 11-phase project lifecycle (P1-P11)
|
|
32
|
+
- CUSTOM: User-defined workflow with selected phases
|
|
33
|
+
"""
|
|
34
|
+
SHORTCUT = "shortcut"
|
|
35
|
+
FULL = "full"
|
|
36
|
+
CUSTOM = "custom"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class PhaseDefinition:
|
|
41
|
+
"""Unified definition for a single lifecycle phase."""
|
|
42
|
+
phase_id: str
|
|
43
|
+
name: str
|
|
44
|
+
description: str
|
|
45
|
+
role_id: str
|
|
46
|
+
dependencies: List[str] = field(default_factory=list)
|
|
47
|
+
artifacts_in: str = ""
|
|
48
|
+
artifacts_out: str = ""
|
|
49
|
+
gate_condition: str = ""
|
|
50
|
+
reviewers: List[str] = field(default_factory=list)
|
|
51
|
+
optional: bool = False
|
|
52
|
+
order: int = 0
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
55
|
+
return {
|
|
56
|
+
"phase_id": self.phase_id,
|
|
57
|
+
"name": self.name,
|
|
58
|
+
"description": self.description,
|
|
59
|
+
"role_id": self.role_id,
|
|
60
|
+
"dependencies": self.dependencies,
|
|
61
|
+
"artifacts_in": self.artifacts_in,
|
|
62
|
+
"artifacts_out": self.artifacts_out,
|
|
63
|
+
"gate_condition": self.gate_condition,
|
|
64
|
+
"reviewers": self.reviewers,
|
|
65
|
+
"optional": self.optional,
|
|
66
|
+
"order": self.order,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PhaseState(Enum):
|
|
71
|
+
"""Phase execution state."""
|
|
72
|
+
PENDING = "pending"
|
|
73
|
+
RUNNING = "running"
|
|
74
|
+
COMPLETED = "completed"
|
|
75
|
+
FAILED = "failed"
|
|
76
|
+
SKIPPED = "skipped"
|
|
77
|
+
BLOCKED = "blocked"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class GateResult:
|
|
82
|
+
"""Result of gate check for a phase."""
|
|
83
|
+
passed: bool
|
|
84
|
+
verdict: str # APPROVE / CONDITIONAL / REJECT
|
|
85
|
+
red_flags: List[Dict[str, Any]] = field(default_factory=list)
|
|
86
|
+
missing_evidence: List[Dict[str, Any]] = field(default_factory=list)
|
|
87
|
+
gap_report: str = ""
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"passed": self.passed,
|
|
92
|
+
"verdict": self.verdict,
|
|
93
|
+
"red_flags_count": len(self.red_flags),
|
|
94
|
+
"missing_evidence_count": len(self.missing_evidence),
|
|
95
|
+
"gap_report": self.gap_report[:200] if self.gap_report else "",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class PhaseResult:
|
|
101
|
+
"""Result of advancing to a phase."""
|
|
102
|
+
success: bool
|
|
103
|
+
phase_id: str
|
|
104
|
+
previous_state: PhaseState
|
|
105
|
+
new_state: PhaseState
|
|
106
|
+
gate_result: Optional[GateResult] = None
|
|
107
|
+
error: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class LifecycleStatus:
|
|
112
|
+
"""Overall lifecycle status."""
|
|
113
|
+
mode: LifecycleMode
|
|
114
|
+
current_phase: Optional[str]
|
|
115
|
+
completed_phases: List[str]
|
|
116
|
+
failed_phases: List[str]
|
|
117
|
+
blocked_phases: List[str]
|
|
118
|
+
progress_percent: float
|
|
119
|
+
can_advance: bool
|
|
120
|
+
next_phase: Optional[str]
|
|
121
|
+
|
|
122
|
+
def to_summary(self) -> str:
|
|
123
|
+
lines = [
|
|
124
|
+
f"Lifecycle Status ({self.mode.value.upper()} Mode)",
|
|
125
|
+
f"Current: {self.current_phase or 'Not started'}",
|
|
126
|
+
f"Progress: {self.progress_percent:.0f}%",
|
|
127
|
+
f"Completed: {len(self.completed_phases)} phases",
|
|
128
|
+
]
|
|
129
|
+
if self.next_phase:
|
|
130
|
+
lines.append(f"Next: {self.next_phase}")
|
|
131
|
+
if not self.can_advance:
|
|
132
|
+
lines.append("⚠️ Blocked: Check gate conditions")
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class ViewMapping:
|
|
138
|
+
"""Maps a CLI command to underlying 11-phase segments."""
|
|
139
|
+
command: str
|
|
140
|
+
phases: List[str] # Phase IDs this command covers
|
|
141
|
+
mode: LifecycleMode = LifecycleMode.SHORTCUT
|
|
142
|
+
description: str = ""
|
|
143
|
+
required_roles: List[str] = field(default_factory=list)
|
|
144
|
+
gate: str = ""
|
|
145
|
+
pre_dispatch_message: str = ""
|
|
146
|
+
|
|
147
|
+
def covers_phase(self, phase_id: str) -> bool:
|
|
148
|
+
return phase_id in self.phases
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class SpecTemplate:
|
|
153
|
+
"""Specification document template."""
|
|
154
|
+
template_id: str
|
|
155
|
+
name: str
|
|
156
|
+
phase_id: str
|
|
157
|
+
sections: List[Dict[str, Any]] = field(default_factory=list)
|
|
158
|
+
required_fields: List[str] = field(default_factory=list)
|
|
159
|
+
validation_rules: List[Dict[str, Any]] = field(default_factory=list)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
SPEC_TEMPLATES: Dict[str, SpecTemplate] = {
|
|
163
|
+
"requirements": SpecTemplate(
|
|
164
|
+
template_id="requirements",
|
|
165
|
+
name="Requirements Specification",
|
|
166
|
+
phase_id="P1",
|
|
167
|
+
sections=[
|
|
168
|
+
{"title": "Objectives", "description": "Project goals and success criteria"},
|
|
169
|
+
{"title": "User Stories", "description": "User stories with acceptance criteria"},
|
|
170
|
+
{"title": "Non-Functional Requirements", "description": "Performance, security, scalability"},
|
|
171
|
+
{"title": "Constraints", "description": "Technical and business constraints"},
|
|
172
|
+
{"title": "Boundaries", "description": "In-scope and out-of-scope items"},
|
|
173
|
+
],
|
|
174
|
+
required_fields=["objectives", "user_stories"],
|
|
175
|
+
validation_rules=[
|
|
176
|
+
{"field": "objectives", "check": "not_empty", "severity": "critical"},
|
|
177
|
+
{"field": "user_stories", "check": "min_count", "value": 1, "severity": "critical"},
|
|
178
|
+
],
|
|
179
|
+
),
|
|
180
|
+
"architecture": SpecTemplate(
|
|
181
|
+
template_id="architecture",
|
|
182
|
+
name="Architecture Specification",
|
|
183
|
+
phase_id="P2",
|
|
184
|
+
sections=[
|
|
185
|
+
{"title": "System Overview", "description": "High-level architecture diagram"},
|
|
186
|
+
{"title": "Tech Stack", "description": "Technology selection and rationale"},
|
|
187
|
+
{"title": "Service Boundaries", "description": "Module/service decomposition"},
|
|
188
|
+
{"title": "Quality Attributes", "description": "Performance, security, reliability targets"},
|
|
189
|
+
],
|
|
190
|
+
required_fields=["system_overview", "tech_stack"],
|
|
191
|
+
validation_rules=[
|
|
192
|
+
{"field": "tech_stack", "check": "not_empty", "severity": "critical"},
|
|
193
|
+
],
|
|
194
|
+
),
|
|
195
|
+
"technical": SpecTemplate(
|
|
196
|
+
template_id="technical",
|
|
197
|
+
name="Technical Specification",
|
|
198
|
+
phase_id="P3",
|
|
199
|
+
sections=[
|
|
200
|
+
{"title": "API Specifications", "description": "Endpoint definitions"},
|
|
201
|
+
{"title": "Interface Definitions", "description": "Internal interfaces"},
|
|
202
|
+
{"title": "Data Models", "description": "Schema definitions"},
|
|
203
|
+
{"title": "Error Handling", "description": "Error codes and recovery"},
|
|
204
|
+
],
|
|
205
|
+
required_fields=["api_specifications"],
|
|
206
|
+
validation_rules=[
|
|
207
|
+
{"field": "api_specifications", "check": "not_empty", "severity": "critical"},
|
|
208
|
+
],
|
|
209
|
+
),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Predefined view mappings (CLI → 11 phases)
|
|
214
|
+
VIEW_MAPPINGS: Dict[str, ViewMapping] = {
|
|
215
|
+
"spec": ViewMapping(
|
|
216
|
+
command="spec",
|
|
217
|
+
phases=["P1", "P2"],
|
|
218
|
+
description="Define requirements and architecture before implementation",
|
|
219
|
+
required_roles=["architect", "product-manager"],
|
|
220
|
+
gate="spec_first",
|
|
221
|
+
pre_dispatch_message=(
|
|
222
|
+
"📋 Generating specification (P1: Requirements + P2: Architecture). "
|
|
223
|
+
"Output will include objectives, structure, testing plan, and boundaries."
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
"plan": ViewMapping(
|
|
227
|
+
command="plan",
|
|
228
|
+
phases=["P7"],
|
|
229
|
+
description="Break down work into verifiable tasks and test plans",
|
|
230
|
+
required_roles=["architect", "product-manager"],
|
|
231
|
+
gate="task_breakdown_complete",
|
|
232
|
+
pre_dispatch_message=(
|
|
233
|
+
"📝 Decomposing into atomic tasks (P7: Test Planning). "
|
|
234
|
+
"Output includes test plan with acceptance criteria."
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
"build": ViewMapping(
|
|
238
|
+
command="build",
|
|
239
|
+
phases=["P8"],
|
|
240
|
+
description="Implement incrementally with TDD discipline",
|
|
241
|
+
required_roles=["architect", "solo-coder", "tester"],
|
|
242
|
+
gate="incremental_verification",
|
|
243
|
+
pre_dispatch_message=(
|
|
244
|
+
"🔨 Building in thin vertical slices (P8: Implementation). "
|
|
245
|
+
"Each slice: implement → test → verify. ~100 lines per slice max."
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
"test": ViewMapping(
|
|
249
|
+
command="test",
|
|
250
|
+
phases=["P9"],
|
|
251
|
+
description="Run tests with mandatory evidence requirements",
|
|
252
|
+
required_roles=["tester", "solo-coder"],
|
|
253
|
+
gate="evidence_required",
|
|
254
|
+
pre_dispatch_message=(
|
|
255
|
+
"🧪 Running tests with verification gate (P9: Test Execution). "
|
|
256
|
+
"Evidence required: test output, build status, diff summary."
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
"review": ViewMapping(
|
|
260
|
+
command="review",
|
|
261
|
+
phases=["P8_review_embedded", "P6_partial"],
|
|
262
|
+
description="Five-axis code review and security checks",
|
|
263
|
+
required_roles=["solo-coder", "security", "tester", "architect"],
|
|
264
|
+
gate="change_size_limit",
|
|
265
|
+
pre_dispatch_message=(
|
|
266
|
+
"🔍 Conducting multi-dimensional review. "
|
|
267
|
+
"Change size target: ~100 lines. Severity labels: Critical/Required/Nit."
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
"ship": ViewMapping(
|
|
271
|
+
command="ship",
|
|
272
|
+
phases=["P10"],
|
|
273
|
+
description="Pre-launch verification and deployment preparation",
|
|
274
|
+
required_roles=["devops", "security", "architect"],
|
|
275
|
+
gate="pre_launch_checklist",
|
|
276
|
+
pre_dispatch_message=(
|
|
277
|
+
"🚀 Running pre-launch checklist across 6 dimensions. "
|
|
278
|
+
"Rollback plan required."
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
"spec-init": ViewMapping(
|
|
282
|
+
command="spec-init",
|
|
283
|
+
phases=["P1"],
|
|
284
|
+
description="Initialize specification document from template",
|
|
285
|
+
required_roles=["architect", "product-manager"],
|
|
286
|
+
gate="spec_template_complete",
|
|
287
|
+
pre_dispatch_message=(
|
|
288
|
+
"📋 Initializing specification document from template. "
|
|
289
|
+
"Choose template: requirements / architecture / technical."
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
"spec-analyze": ViewMapping(
|
|
293
|
+
command="spec-analyze",
|
|
294
|
+
phases=["P1", "P2"],
|
|
295
|
+
description="Analyze existing codebase to generate specification draft",
|
|
296
|
+
required_roles=["architect"],
|
|
297
|
+
gate="code_analysis_complete",
|
|
298
|
+
pre_dispatch_message=(
|
|
299
|
+
"🔍 Analyzing codebase structure to generate specification draft. "
|
|
300
|
+
"Uses CodeMapGenerator for multi-language analysis."
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
"spec-validate": ViewMapping(
|
|
304
|
+
command="spec-validate",
|
|
305
|
+
phases=["P1", "P2", "P3"],
|
|
306
|
+
description="Validate specification completeness and consistency",
|
|
307
|
+
required_roles=["architect", "tester"],
|
|
308
|
+
gate="spec_validation_passed",
|
|
309
|
+
pre_dispatch_message=(
|
|
310
|
+
"✅ Validating specification against completeness rules. "
|
|
311
|
+
"Checks required fields, consistency, and coverage."
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class LifecycleProtocol(ABC):
|
|
318
|
+
"""
|
|
319
|
+
Abstract interface for unified lifecycle management.
|
|
320
|
+
|
|
321
|
+
This protocol decouples the view layer (CLI 6 commands) from the core engine
|
|
322
|
+
(WorkflowEngine with 11 phases), enabling both SHORTCUT and FULL modes.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
@abstractmethod
|
|
326
|
+
def get_mode(self) -> LifecycleMode:
|
|
327
|
+
"""Return current lifecycle mode."""
|
|
328
|
+
...
|
|
329
|
+
|
|
330
|
+
@abstractmethod
|
|
331
|
+
def set_mode(self, mode: LifecycleMode) -> None:
|
|
332
|
+
"""Switch lifecycle mode."""
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
@abstractmethod
|
|
336
|
+
def get_all_phases(self) -> List[PhaseDefinition]:
|
|
337
|
+
"""Return all available phases in current mode."""
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
@abstractmethod
|
|
341
|
+
def get_active_phases(self) -> List[PhaseDefinition]:
|
|
342
|
+
"""Return phases active for the current task/context."""
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
def get_phase(self, phase_id: str) -> Optional[PhaseDefinition]:
|
|
347
|
+
"""Return specific phase by ID, or None if not found."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
@abstractmethod
|
|
351
|
+
def get_current_phase(self) -> Optional[PhaseDefinition]:
|
|
352
|
+
"""Return current phase, or None if not started."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
@abstractmethod
|
|
356
|
+
def advance_to_phase(self, phase_id: str) -> PhaseResult:
|
|
357
|
+
"""Advance to specified phase, running gate checks."""
|
|
358
|
+
...
|
|
359
|
+
|
|
360
|
+
@abstractmethod
|
|
361
|
+
def check_gate(self, phase_id: Optional[str] = None) -> GateResult:
|
|
362
|
+
"""Check gate conditions for phase (default: current)."""
|
|
363
|
+
...
|
|
364
|
+
|
|
365
|
+
@abstractmethod
|
|
366
|
+
def get_status(self) -> LifecycleStatus:
|
|
367
|
+
"""Return overall lifecycle status."""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
@abstractmethod
|
|
371
|
+
def get_view_mapping(self, command: str) -> Optional[ViewMapping]:
|
|
372
|
+
"""Get view mapping for a CLI command (SHORTCUT mode only)."""
|
|
373
|
+
...
|
|
374
|
+
|
|
375
|
+
@abstractmethod
|
|
376
|
+
def resolve_command_to_phases(self, command: str) -> List[PhaseDefinition]:
|
|
377
|
+
"""Resolve a CLI command to its underlying phase definitions."""
|
|
378
|
+
...
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ShortcutLifecycleAdapter(LifecycleProtocol):
|
|
382
|
+
"""
|
|
383
|
+
Adapter that implements LifecycleProtocol using CLI 6-command shortcuts.
|
|
384
|
+
|
|
385
|
+
This is the default implementation for SHORTCUT mode.
|
|
386
|
+
Maps CLI commands to 11-phase segments via VIEW_MAPPINGS.
|
|
387
|
+
|
|
388
|
+
Integration (Plan C):
|
|
389
|
+
- Uses UnifiedGateEngine for real gate checks
|
|
390
|
+
- Integrates with CheckpointManager for state persistence
|
|
391
|
+
- Supports lifecycle state save/restore
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
def __init__(self, use_unified_gate: bool = True):
|
|
395
|
+
self._mode = LifecycleMode.SHORTCUT
|
|
396
|
+
self._current_phase: Optional[str] = None
|
|
397
|
+
self._completed_phases: List[str] = []
|
|
398
|
+
self._phase_states: Dict[str, PhaseState] = {}
|
|
399
|
+
self._use_unified_gate = use_unified_gate
|
|
400
|
+
self._gate_engine = None
|
|
401
|
+
self._checkpoint_manager = None
|
|
402
|
+
self._task_id: Optional[str] = None
|
|
403
|
+
|
|
404
|
+
# Initialize unified gate engine if requested
|
|
405
|
+
if use_unified_gate:
|
|
406
|
+
try:
|
|
407
|
+
from scripts.collaboration.unified_gate_engine import (
|
|
408
|
+
UnifiedGateEngine,
|
|
409
|
+
GateType,
|
|
410
|
+
PhaseGateContext,
|
|
411
|
+
get_shared_gate_engine,
|
|
412
|
+
)
|
|
413
|
+
self._gate_engine = get_shared_gate_engine()
|
|
414
|
+
self._GateType = GateType
|
|
415
|
+
self._PhaseGateContext = PhaseGateContext
|
|
416
|
+
logger.debug("UnifiedGateEngine initialized for ShortcutLifecycleAdapter")
|
|
417
|
+
except ImportError as e:
|
|
418
|
+
logger.warning("UnifiedGateEngine not available: %s", e)
|
|
419
|
+
self._use_unified_gate = False
|
|
420
|
+
|
|
421
|
+
def set_task_id(self, task_id: str) -> None:
|
|
422
|
+
"""Set task ID for checkpoint integration."""
|
|
423
|
+
self._task_id = task_id
|
|
424
|
+
if not self._checkpoint_manager:
|
|
425
|
+
try:
|
|
426
|
+
from scripts.collaboration.checkpoint_manager import CheckpointManager
|
|
427
|
+
self._checkpoint_manager = CheckpointManager()
|
|
428
|
+
logger.info("CheckpointManager initialized for task %s", task_id)
|
|
429
|
+
except ImportError as e:
|
|
430
|
+
logger.warning("CheckpointManager not available: %s", e)
|
|
431
|
+
|
|
432
|
+
def enable_checkpoint_integration(self, storage_path: str = "./checkpoints") -> bool:
|
|
433
|
+
"""
|
|
434
|
+
Enable checkpoint manager integration for state persistence.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
storage_path: Path for storing checkpoints and lifecycle state
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
True if successfully enabled
|
|
441
|
+
"""
|
|
442
|
+
try:
|
|
443
|
+
from scripts.collaboration.checkpoint_manager import CheckpointManager
|
|
444
|
+
self._checkpoint_manager = CheckpointManager(storage_path=storage_path)
|
|
445
|
+
logger.info(
|
|
446
|
+
"CheckpointManager enabled at %s",
|
|
447
|
+
storage_path,
|
|
448
|
+
)
|
|
449
|
+
return True
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.warning("Failed to enable checkpoint integration: %s", e)
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
def save_state(self) -> bool:
|
|
455
|
+
"""
|
|
456
|
+
Save current lifecycle state to checkpoint manager.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
True if saved successfully
|
|
460
|
+
"""
|
|
461
|
+
if self._checkpoint_manager and self._task_id:
|
|
462
|
+
try:
|
|
463
|
+
phase_states_str = {
|
|
464
|
+
pid: (state.value if hasattr(state, 'value') else str(state))
|
|
465
|
+
for pid, state in self._phase_states.items()
|
|
466
|
+
}
|
|
467
|
+
mode_str = self._mode.value if hasattr(self._mode, 'value') else str(self._mode)
|
|
468
|
+
|
|
469
|
+
return self._checkpoint_manager.save_lifecycle_state(
|
|
470
|
+
task_id=self._task_id,
|
|
471
|
+
current_phase=self._current_phase,
|
|
472
|
+
phase_states=phase_states_str,
|
|
473
|
+
completed_phases=self._completed_phases.copy(),
|
|
474
|
+
mode=mode_str,
|
|
475
|
+
)
|
|
476
|
+
except Exception as e:
|
|
477
|
+
logger.warning("Failed to save lifecycle state: %s", e)
|
|
478
|
+
return False
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def restore_state(self) -> bool:
|
|
482
|
+
"""
|
|
483
|
+
Restore lifecycle state from checkpoint manager.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
True if restored successfully
|
|
487
|
+
"""
|
|
488
|
+
if self._checkpoint_manager and self._task_id:
|
|
489
|
+
try:
|
|
490
|
+
state = self._checkpoint_manager.load_lifecycle_state(self._task_id)
|
|
491
|
+
if state:
|
|
492
|
+
self._current_phase = state.get("current_phase")
|
|
493
|
+
self._completed_phases = state.get("completed_phases", [])
|
|
494
|
+
|
|
495
|
+
phase_states_raw = state.get("phase_states", {})
|
|
496
|
+
self._phase_states = {}
|
|
497
|
+
for pid, pstate in phase_states_raw.items():
|
|
498
|
+
try:
|
|
499
|
+
from scripts.collaboration.lifecycle_protocol import PhaseState
|
|
500
|
+
self._phase_states[pid] = PhaseState(pstate)
|
|
501
|
+
except ValueError:
|
|
502
|
+
self._phase_states[pid] = PhaseState.PENDING
|
|
503
|
+
|
|
504
|
+
mode_raw = state.get("mode", "shortcut")
|
|
505
|
+
try:
|
|
506
|
+
from scripts.collaboration.lifecycle_protocol import LifecycleMode
|
|
507
|
+
self._mode = LifecycleMode(mode_raw)
|
|
508
|
+
except ValueError:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
logger.info(
|
|
512
|
+
"Restored lifecycle state: task=%s, phase=%s",
|
|
513
|
+
self._task_id,
|
|
514
|
+
self._current_phase,
|
|
515
|
+
)
|
|
516
|
+
return True
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.warning("Failed to restore lifecycle state: %s", e)
|
|
519
|
+
return False
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
def get_mode(self) -> LifecycleMode:
|
|
523
|
+
return self._mode
|
|
524
|
+
|
|
525
|
+
def set_mode(self, mode: LifecycleMode) -> None:
|
|
526
|
+
self._mode = mode
|
|
527
|
+
|
|
528
|
+
def get_all_phases(self) -> List[PhaseDefinition]:
|
|
529
|
+
from scripts.collaboration.workflow_engine import PHASE_TEMPLATES
|
|
530
|
+
|
|
531
|
+
phases = []
|
|
532
|
+
for pid, ptmpl in PHASE_TEMPLATES.items():
|
|
533
|
+
phases.append(PhaseDefinition(
|
|
534
|
+
phase_id=pid,
|
|
535
|
+
name=ptmpl.get("name", pid),
|
|
536
|
+
description=ptmpl.get("description", ""),
|
|
537
|
+
role_id=ptmpl.get("role_id", ""),
|
|
538
|
+
dependencies=ptmpl.get("dependencies", []),
|
|
539
|
+
artifacts_in=ptmpl.get("artifacts_in", ""),
|
|
540
|
+
artifacts_out=ptmpl.get("artifacts_out", ""),
|
|
541
|
+
gate_condition=ptmpl.get("gate_condition", ""),
|
|
542
|
+
reviewers=ptmpl.get("reviewers", []),
|
|
543
|
+
optional=ptmpl.get("optional", False),
|
|
544
|
+
))
|
|
545
|
+
return sorted(phases, key=lambda p: p.phase_id)
|
|
546
|
+
|
|
547
|
+
def get_active_phases(self) -> List[PhaseDefinition]:
|
|
548
|
+
if self._mode == LifecycleMode.SHORTCUT and self._current_phase:
|
|
549
|
+
mapping = self._get_current_mapping()
|
|
550
|
+
if mapping:
|
|
551
|
+
return [
|
|
552
|
+
p for p in self.get_all_phases()
|
|
553
|
+
if mapping.covers_phase(p.phase_id)
|
|
554
|
+
]
|
|
555
|
+
return self.get_all_phases()
|
|
556
|
+
|
|
557
|
+
def get_phase(self, phase_id: str) -> Optional[PhaseDefinition]:
|
|
558
|
+
for p in self.get_all_phases():
|
|
559
|
+
if p.phase_id == phase_id:
|
|
560
|
+
return p
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
def get_current_phase(self) -> Optional[PhaseDefinition]:
|
|
564
|
+
if self._current_phase:
|
|
565
|
+
return self.get_phase(self._current_phase)
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
def advance_to_phase(self, phase_id: str) -> PhaseResult:
|
|
569
|
+
prev_state = self._phase_states.get(phase_id, PhaseState.PENDING)
|
|
570
|
+
|
|
571
|
+
# First check if already completed (idempotent)
|
|
572
|
+
if prev_state == PhaseState.COMPLETED:
|
|
573
|
+
return PhaseResult(
|
|
574
|
+
success=True,
|
|
575
|
+
phase_id=phase_id,
|
|
576
|
+
previous_state=prev_state,
|
|
577
|
+
new_state=PhaseState.COMPLETED,
|
|
578
|
+
gate_result=GateResult(passed=True, verdict="APPROVE"),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Check dependencies first (before gate)
|
|
582
|
+
phase_def = self.get_phase(phase_id)
|
|
583
|
+
unmet_deps = []
|
|
584
|
+
if phase_def:
|
|
585
|
+
unmet_deps = [d for d in phase_def.dependencies if d not in self._completed_phases]
|
|
586
|
+
if unmet_deps:
|
|
587
|
+
result = PhaseResult(
|
|
588
|
+
success=False,
|
|
589
|
+
phase_id=phase_id,
|
|
590
|
+
previous_state=prev_state,
|
|
591
|
+
new_state=PhaseState.BLOCKED,
|
|
592
|
+
gate_result=GateResult(
|
|
593
|
+
passed=False,
|
|
594
|
+
verdict="CONDITIONAL",
|
|
595
|
+
missing_evidence=[{"dependency": d} for d in unmet_deps],
|
|
596
|
+
gap_report=f"Unmet dependencies: {', '.join(unmet_deps)}",
|
|
597
|
+
),
|
|
598
|
+
error=f"Unmet dependencies: {unmet_deps}",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Auto-save state on block
|
|
602
|
+
self.save_state()
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
# Run gate check using unified engine or fallback
|
|
606
|
+
gate_result = self.check_gate(phase_id)
|
|
607
|
+
if not gate_result.passed and gate_result.verdict == "REJECT":
|
|
608
|
+
self._phase_states[phase_id] = PhaseState.BLOCKED
|
|
609
|
+
result = PhaseResult(
|
|
610
|
+
success=False,
|
|
611
|
+
phase_id=phase_id,
|
|
612
|
+
previous_state=prev_state,
|
|
613
|
+
new_state=PhaseState.BLOCKED,
|
|
614
|
+
gate_result=gate_result,
|
|
615
|
+
error=f"Gate rejected: {gate_result.gap_report}",
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Auto-save state on rejection
|
|
619
|
+
self.save_state()
|
|
620
|
+
return result
|
|
621
|
+
|
|
622
|
+
# Advance to RUNNING state
|
|
623
|
+
self._current_phase = phase_id
|
|
624
|
+
self._phase_states[phase_id] = PhaseState.RUNNING
|
|
625
|
+
logger.debug("Advanced to phase %s, _current_phase=%s", phase_id, self._current_phase)
|
|
626
|
+
|
|
627
|
+
# Auto-save state on successful advance
|
|
628
|
+
self.save_state()
|
|
629
|
+
|
|
630
|
+
return PhaseResult(
|
|
631
|
+
success=True,
|
|
632
|
+
phase_id=phase_id,
|
|
633
|
+
previous_state=prev_state,
|
|
634
|
+
new_state=PhaseState.RUNNING,
|
|
635
|
+
gate_result=gate_result,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def complete_phase(self, phase_id: str) -> None:
|
|
639
|
+
"""Mark a phase as completed."""
|
|
640
|
+
self._phase_states[phase_id] = PhaseState.COMPLETED
|
|
641
|
+
if phase_id not in self._completed_phases:
|
|
642
|
+
self._completed_phases.append(phase_id)
|
|
643
|
+
|
|
644
|
+
# Auto-save state on completion
|
|
645
|
+
self.save_state()
|
|
646
|
+
|
|
647
|
+
def check_gate(self, phase_id: Optional[str] = None) -> GateResult:
|
|
648
|
+
target = phase_id or self._current_phase
|
|
649
|
+
if not target:
|
|
650
|
+
return GateResult(passed=True, verdict="APPROVE")
|
|
651
|
+
|
|
652
|
+
phase_def = self.get_phase(target)
|
|
653
|
+
if not phase_def:
|
|
654
|
+
return GateResult(passed=False, verdict="REJECT", gap_report=f"Phase {target} not found")
|
|
655
|
+
|
|
656
|
+
# Use UnifiedGateEngine if available
|
|
657
|
+
if self._use_unified_gate and self._gate_engine:
|
|
658
|
+
try:
|
|
659
|
+
return self._check_gate_with_unified_engine(target, phase_def)
|
|
660
|
+
except Exception as e:
|
|
661
|
+
logger.warning("UnifiedGateEngine failed, falling back: %s", e)
|
|
662
|
+
|
|
663
|
+
# Fallback to basic gate checks
|
|
664
|
+
return self._check_gate_basic(target, phase_def)
|
|
665
|
+
|
|
666
|
+
def _check_gate_with_unified_engine(
|
|
667
|
+
self,
|
|
668
|
+
target: str,
|
|
669
|
+
phase_def: PhaseDefinition,
|
|
670
|
+
) -> GateResult:
|
|
671
|
+
"""Check gate using UnifiedGateEngine."""
|
|
672
|
+
# Build phase context
|
|
673
|
+
unmet_deps = [d for d in phase_def.dependencies if d not in self._completed_phases]
|
|
674
|
+
|
|
675
|
+
context = self._PhaseGateContext(
|
|
676
|
+
phase_id=target,
|
|
677
|
+
phase_name=phase_def.name,
|
|
678
|
+
current_state=self._phase_states.get(target, PhaseState.PENDING).value,
|
|
679
|
+
target_state="running",
|
|
680
|
+
dependencies_met=len(unmet_deps) == 0,
|
|
681
|
+
completed_phases=self._completed_phases.copy(),
|
|
682
|
+
unmet_dependencies=unmet_deps,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Run unified gate check
|
|
686
|
+
unified_result = self._gate_engine.check(
|
|
687
|
+
gate_type=self._GateType.PHASE_TRANSITION,
|
|
688
|
+
context=context,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Convert UnifiedGateResult to GateResult
|
|
692
|
+
return GateResult(
|
|
693
|
+
passed=unified_result.passed,
|
|
694
|
+
verdict=unified_result.verdict,
|
|
695
|
+
red_flags=[
|
|
696
|
+
{"id": issue.get("code", "unknown"), "severity": "critical", **issue}
|
|
697
|
+
for issue in unified_result.critical_issues
|
|
698
|
+
],
|
|
699
|
+
missing_evidence=[
|
|
700
|
+
{"key": ev, "required": True}
|
|
701
|
+
for ev in unified_result.evidence_required
|
|
702
|
+
],
|
|
703
|
+
gap_report="\n".join([
|
|
704
|
+
f"- [{issue.get('severity', 'unknown')}] {issue.get('message', '')}"
|
|
705
|
+
for issue in unified_result.critical_issues + unified_result.warnings
|
|
706
|
+
]) if (unified_result.critical_issues or unified_result.warnings) else "",
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
def _check_gate_basic(
|
|
710
|
+
self,
|
|
711
|
+
target: str,
|
|
712
|
+
phase_def: PhaseDefinition,
|
|
713
|
+
) -> GateResult:
|
|
714
|
+
"""Basic fallback gate check without UnifiedGateEngine."""
|
|
715
|
+
state = self._phase_states.get(target, PhaseState.PENDING)
|
|
716
|
+
if state == PhaseState.BLOCKED:
|
|
717
|
+
return GateResult(passed=False, verdict="REJECT", gap_report="Phase is blocked")
|
|
718
|
+
if state == PhaseState.COMPLETED:
|
|
719
|
+
return GateResult(passed=True, verdict="APPROVE")
|
|
720
|
+
|
|
721
|
+
# Check dependencies
|
|
722
|
+
unmet_deps = [d for d in phase_def.dependencies if d not in self._completed_phases]
|
|
723
|
+
if unmet_deps:
|
|
724
|
+
return GateResult(
|
|
725
|
+
passed=False,
|
|
726
|
+
verdict="CONDITIONAL",
|
|
727
|
+
missing_evidence=[{"dependency": d} for d in unmet_deps],
|
|
728
|
+
gap_report=f"Unmet dependencies: {', '.join(unmet_deps)}",
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return GateResult(passed=True, verdict="APPROVE")
|
|
732
|
+
|
|
733
|
+
def get_status(self) -> LifecycleStatus:
|
|
734
|
+
all_phases = self.get_all_phases()
|
|
735
|
+
total = len(all_phases)
|
|
736
|
+
completed = len(self._completed_phases)
|
|
737
|
+
progress = (completed / total * 100) if total > 0 else 0.0
|
|
738
|
+
|
|
739
|
+
next_phase = None
|
|
740
|
+
can_advance = True
|
|
741
|
+
for p in all_phases:
|
|
742
|
+
pid = p.phase_id
|
|
743
|
+
state = self._phase_states.get(pid, PhaseState.PENDING)
|
|
744
|
+
if state == PhaseState.PENDING and not next_phase:
|
|
745
|
+
next_phase = pid
|
|
746
|
+
if state == PhaseState.BLOCKED:
|
|
747
|
+
can_advance = False
|
|
748
|
+
|
|
749
|
+
return LifecycleStatus(
|
|
750
|
+
mode=self._mode,
|
|
751
|
+
current_phase=self._current_phase,
|
|
752
|
+
completed_phases=self._completed_phases.copy(),
|
|
753
|
+
failed_phases=[pid for pid, s in self._phase_states.items() if s == PhaseState.FAILED],
|
|
754
|
+
blocked_phases=[pid for pid, s in self._phase_states.items() if s == PhaseState.BLOCKED],
|
|
755
|
+
progress_percent=progress,
|
|
756
|
+
can_advance=can_advance,
|
|
757
|
+
next_phase=next_phase,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
def get_view_mapping(self, command: str) -> Optional[ViewMapping]:
|
|
761
|
+
return VIEW_MAPPINGS.get(command)
|
|
762
|
+
|
|
763
|
+
def resolve_command_to_phases(self, command: str) -> List[PhaseDefinition]:
|
|
764
|
+
mapping = VIEW_MAPPINGS.get(command)
|
|
765
|
+
if not mapping:
|
|
766
|
+
return []
|
|
767
|
+
|
|
768
|
+
return [p for p in self.get_all_phases() if mapping.covers_phase(p.phase_id)]
|
|
769
|
+
|
|
770
|
+
def _get_current_mapping(self) -> Optional[ViewMapping]:
|
|
771
|
+
if self._current_phase:
|
|
772
|
+
for mapping in VIEW_MAPPINGS.values():
|
|
773
|
+
if mapping.covers_phase(self._current_phase):
|
|
774
|
+
return mapping
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
def init_spec(
|
|
778
|
+
self,
|
|
779
|
+
template_id: str = "requirements",
|
|
780
|
+
project_info: Optional[Dict[str, Any]] = None,
|
|
781
|
+
) -> Dict[str, Any]:
|
|
782
|
+
"""
|
|
783
|
+
Initialize a specification document from template.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
template_id: Template type (requirements/architecture/technical)
|
|
787
|
+
project_info: Optional project information to pre-fill
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Dict with success status and spec document structure
|
|
791
|
+
"""
|
|
792
|
+
template = SPEC_TEMPLATES.get(template_id)
|
|
793
|
+
if not template:
|
|
794
|
+
return {"success": False, "error": f"Unknown template: {template_id}"}
|
|
795
|
+
|
|
796
|
+
doc = {
|
|
797
|
+
"template_id": template_id,
|
|
798
|
+
"name": template.name,
|
|
799
|
+
"phase_id": template.phase_id,
|
|
800
|
+
"sections": {},
|
|
801
|
+
}
|
|
802
|
+
for section in template.sections:
|
|
803
|
+
doc["sections"][section["title"]] = {
|
|
804
|
+
"description": section["description"],
|
|
805
|
+
"content": "",
|
|
806
|
+
"status": "draft",
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if project_info:
|
|
810
|
+
doc["project_info"] = project_info
|
|
811
|
+
|
|
812
|
+
return {"success": True, "spec": doc, "template_id": template_id}
|
|
813
|
+
|
|
814
|
+
def analyze_spec(
|
|
815
|
+
self,
|
|
816
|
+
source_dir: str = ".",
|
|
817
|
+
template_id: str = "requirements",
|
|
818
|
+
) -> Dict[str, Any]:
|
|
819
|
+
"""
|
|
820
|
+
Analyze existing codebase to generate specification draft.
|
|
821
|
+
|
|
822
|
+
Uses CodeMapGenerator for multi-language code analysis.
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
source_dir: Root directory to analyze
|
|
826
|
+
template_id: Template to use for structuring the analysis
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
Dict with success status and analysis results
|
|
830
|
+
"""
|
|
831
|
+
try:
|
|
832
|
+
from scripts.collaboration.code_map_generator import CodeMapGenerator
|
|
833
|
+
from scripts.collaboration.language_parsers import DEFAULT_PARSERS
|
|
834
|
+
|
|
835
|
+
gen = CodeMapGenerator(project_root=source_dir, parsers=DEFAULT_PARSERS)
|
|
836
|
+
code_map = gen.generate_map(output_format="dict")
|
|
837
|
+
dep_graph = gen.get_dependency_graph()
|
|
838
|
+
|
|
839
|
+
analysis = {
|
|
840
|
+
"modules": list(code_map.keys()),
|
|
841
|
+
"total_modules": len(code_map),
|
|
842
|
+
"total_classes": sum(
|
|
843
|
+
m.get("total_classes", 0) for m in code_map.values()
|
|
844
|
+
),
|
|
845
|
+
"total_functions": sum(
|
|
846
|
+
m.get("total_functions", 0) for m in code_map.values()
|
|
847
|
+
),
|
|
848
|
+
"dependencies": dep_graph,
|
|
849
|
+
"languages": list(
|
|
850
|
+
set(
|
|
851
|
+
m.get("language", "python")
|
|
852
|
+
for m in code_map.values()
|
|
853
|
+
if isinstance(m, dict)
|
|
854
|
+
)
|
|
855
|
+
),
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return {"success": True, "analysis": analysis, "template_id": template_id}
|
|
859
|
+
except Exception as e:
|
|
860
|
+
return {"success": False, "error": str(e)}
|
|
861
|
+
|
|
862
|
+
def validate_spec(
|
|
863
|
+
self,
|
|
864
|
+
spec_path: Optional[str] = None,
|
|
865
|
+
template_id: Optional[str] = None,
|
|
866
|
+
spec_data: Optional[Dict[str, Any]] = None,
|
|
867
|
+
) -> Dict[str, Any]:
|
|
868
|
+
"""
|
|
869
|
+
Validate specification completeness and consistency.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
spec_path: Path to specification file (optional)
|
|
873
|
+
template_id: Template to validate against
|
|
874
|
+
spec_data: Specification data dict to validate (optional)
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Dict with validation results
|
|
878
|
+
"""
|
|
879
|
+
results: Dict[str, Any] = {"valid": True, "errors": [], "warnings": []}
|
|
880
|
+
|
|
881
|
+
template = SPEC_TEMPLATES.get(template_id or "requirements")
|
|
882
|
+
if not template:
|
|
883
|
+
return {
|
|
884
|
+
"valid": False,
|
|
885
|
+
"errors": [{"message": f"Unknown template: {template_id}"}],
|
|
886
|
+
"warnings": [],
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if spec_data is None:
|
|
890
|
+
if spec_path:
|
|
891
|
+
try:
|
|
892
|
+
raw = Path(spec_path).read_text(encoding="utf-8")
|
|
893
|
+
spec_data = json.loads(raw)
|
|
894
|
+
except Exception:
|
|
895
|
+
spec_data = {}
|
|
896
|
+
else:
|
|
897
|
+
spec_data = {}
|
|
898
|
+
|
|
899
|
+
sections = spec_data.get("sections", spec_data)
|
|
900
|
+
|
|
901
|
+
for field_name in template.required_fields:
|
|
902
|
+
value = sections.get(field_name, spec_data.get(field_name, ""))
|
|
903
|
+
if not value or (isinstance(value, str) and not value.strip()):
|
|
904
|
+
results["errors"].append(
|
|
905
|
+
{
|
|
906
|
+
"field": field_name,
|
|
907
|
+
"message": f"Required field '{field_name}' is missing or empty",
|
|
908
|
+
"severity": "critical",
|
|
909
|
+
}
|
|
910
|
+
)
|
|
911
|
+
results["valid"] = False
|
|
912
|
+
|
|
913
|
+
for rule in template.validation_rules:
|
|
914
|
+
field_name = rule.get("field", "")
|
|
915
|
+
check = rule.get("check", "")
|
|
916
|
+
severity = rule.get("severity", "warning")
|
|
917
|
+
value = sections.get(field_name, spec_data.get(field_name, ""))
|
|
918
|
+
if check == "not_empty" and (not value or (isinstance(value, str) and not value.strip())):
|
|
919
|
+
results["warnings"].append(
|
|
920
|
+
{
|
|
921
|
+
"field": field_name,
|
|
922
|
+
"message": f"Field '{field_name}' should not be empty",
|
|
923
|
+
"severity": severity,
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
return results
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def create_lifecycle_protocol(mode: LifecycleMode = LifecycleMode.SHORTCUT) -> LifecycleProtocol:
|
|
931
|
+
"""Factory function to create appropriate protocol implementation."""
|
|
932
|
+
if mode == LifecycleMode.FULL:
|
|
933
|
+
return FullLifecycleAdapter()
|
|
934
|
+
else:
|
|
935
|
+
adapter = ShortcutLifecycleAdapter()
|
|
936
|
+
adapter.set_mode(mode)
|
|
937
|
+
return adapter
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def get_shared_protocol() -> LifecycleProtocol:
|
|
941
|
+
"""Get shared singleton instance of lifecycle protocol."""
|
|
942
|
+
if not hasattr(get_shared_protocol, "_instance"):
|
|
943
|
+
get_shared_protocol._instance = create_lifecycle_protocol()
|
|
944
|
+
return get_shared_protocol._instance
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
class FullLifecycleAdapter(LifecycleProtocol):
|
|
948
|
+
"""
|
|
949
|
+
Full 11-phase lifecycle adapter for complete project lifecycles.
|
|
950
|
+
|
|
951
|
+
Implements LifecycleProtocol with FULL mode, supporting all P1-P11 phases
|
|
952
|
+
with automatic dependency resolution and phase ordering.
|
|
953
|
+
|
|
954
|
+
Features:
|
|
955
|
+
- Complete P1-P11 phase support
|
|
956
|
+
- Automatic dependency resolution
|
|
957
|
+
- Phase ordering based on dependency graph
|
|
958
|
+
- Optional phase skipping (for non-required phases)
|
|
959
|
+
- Integration with UnifiedGateEngine and CheckpointManager
|
|
960
|
+
|
|
961
|
+
Use Cases:
|
|
962
|
+
- Large projects requiring full lifecycle management
|
|
963
|
+
- Compliance-critical workflows (all phases mandatory)
|
|
964
|
+
- Projects with complex dependencies between phases
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
def __init__(self, use_unified_gate: bool = True):
|
|
968
|
+
self._mode = LifecycleMode.FULL
|
|
969
|
+
self._current_phase: Optional[str] = None
|
|
970
|
+
self._completed_phases: List[str] = []
|
|
971
|
+
self._phase_states: Dict[str, PhaseState] = {}
|
|
972
|
+
self._use_unified_gate = use_unified_gate
|
|
973
|
+
self._gate_engine = None
|
|
974
|
+
self._checkpoint_manager = None
|
|
975
|
+
self._task_id: Optional[str] = None
|
|
976
|
+
self._skip_optional: bool = False
|
|
977
|
+
self._execution_order: List[str] = []
|
|
978
|
+
|
|
979
|
+
# Initialize unified gate engine if requested
|
|
980
|
+
if use_unified_gate:
|
|
981
|
+
try:
|
|
982
|
+
from scripts.collaboration.unified_gate_engine import (
|
|
983
|
+
UnifiedGateEngine,
|
|
984
|
+
GateType,
|
|
985
|
+
PhaseGateContext,
|
|
986
|
+
get_shared_gate_engine,
|
|
987
|
+
)
|
|
988
|
+
self._gate_engine = get_shared_gate_engine()
|
|
989
|
+
self._GateType = GateType
|
|
990
|
+
self._PhaseGateContext = PhaseGateContext
|
|
991
|
+
logger.debug("UnifiedGateEngine initialized for FullLifecycleAdapter")
|
|
992
|
+
except ImportError as e:
|
|
993
|
+
logger.warning("UnifiedGateEngine not available: %s", e)
|
|
994
|
+
self._use_unified_gate = False
|
|
995
|
+
|
|
996
|
+
# Build execution order from dependencies
|
|
997
|
+
self._build_execution_order()
|
|
998
|
+
|
|
999
|
+
def _build_execution_order(self) -> None:
|
|
1000
|
+
"""Build topological execution order based on phase dependencies."""
|
|
1001
|
+
from scripts.collaboration.workflow_engine import PHASE_TEMPLATES
|
|
1002
|
+
|
|
1003
|
+
# Simple topological sort
|
|
1004
|
+
visited = set()
|
|
1005
|
+
order = []
|
|
1006
|
+
|
|
1007
|
+
def visit(phase_id: str):
|
|
1008
|
+
if phase_id in visited:
|
|
1009
|
+
return
|
|
1010
|
+
visited.add(phase_id)
|
|
1011
|
+
|
|
1012
|
+
phase = PHASE_TEMPLATES.get(phase_id, {})
|
|
1013
|
+
for dep in phase.get("dependencies", []):
|
|
1014
|
+
visit(dep)
|
|
1015
|
+
|
|
1016
|
+
order.append(phase_id)
|
|
1017
|
+
|
|
1018
|
+
# Visit all phases in sorted order for deterministic output
|
|
1019
|
+
for pid in sorted(PHASE_TEMPLATES.keys()):
|
|
1020
|
+
visit(pid)
|
|
1021
|
+
|
|
1022
|
+
self._execution_order = order
|
|
1023
|
+
logger.debug("Full execution order: %s", self._execution_order)
|
|
1024
|
+
|
|
1025
|
+
def set_task_id(self, task_id: str) -> None:
|
|
1026
|
+
"""Set task ID for checkpoint integration."""
|
|
1027
|
+
self._task_id = task_id
|
|
1028
|
+
if not self._checkpoint_manager:
|
|
1029
|
+
try:
|
|
1030
|
+
from scripts.collaboration.checkpoint_manager import CheckpointManager
|
|
1031
|
+
self._checkpoint_manager = CheckpointManager()
|
|
1032
|
+
logger.info("CheckpointManager initialized for task %s", task_id)
|
|
1033
|
+
except ImportError as e:
|
|
1034
|
+
logger.warning("CheckpointManager not available: %s", e)
|
|
1035
|
+
|
|
1036
|
+
def enable_checkpoint_integration(self, storage_path: str = "./checkpoints") -> bool:
|
|
1037
|
+
"""Enable checkpoint manager integration for state persistence."""
|
|
1038
|
+
try:
|
|
1039
|
+
from scripts.collaboration.checkpoint_manager import CheckpointManager
|
|
1040
|
+
self._checkpoint_manager = CheckpointManager(storage_path=storage_path)
|
|
1041
|
+
logger.info("CheckpointManager enabled at %s", storage_path)
|
|
1042
|
+
return True
|
|
1043
|
+
except Exception as e:
|
|
1044
|
+
logger.warning("Failed to enable checkpoint integration: %s", e)
|
|
1045
|
+
return False
|
|
1046
|
+
|
|
1047
|
+
def set_skip_optional(self, skip: bool) -> None:
|
|
1048
|
+
"""Configure whether to skip optional phases (P4, P5, P6, P11)."""
|
|
1049
|
+
self._skip_optional = skip
|
|
1050
|
+
logger.info("Skip optional phases: %s", skip)
|
|
1051
|
+
|
|
1052
|
+
def save_state(self) -> bool:
|
|
1053
|
+
"""Save current lifecycle state to checkpoint manager."""
|
|
1054
|
+
if self._checkpoint_manager and self._task_id:
|
|
1055
|
+
try:
|
|
1056
|
+
phase_states_str = {
|
|
1057
|
+
pid: (state.value if hasattr(state, 'value') else str(state))
|
|
1058
|
+
for pid, state in self._phase_states.items()
|
|
1059
|
+
}
|
|
1060
|
+
mode_str = self._mode.value if hasattr(self._mode, 'value') else str(self._mode)
|
|
1061
|
+
|
|
1062
|
+
return self._checkpoint_manager.save_lifecycle_state(
|
|
1063
|
+
task_id=self._task_id,
|
|
1064
|
+
current_phase=self._current_phase,
|
|
1065
|
+
phase_states=phase_states_str,
|
|
1066
|
+
completed_phases=self._completed_phases.copy(),
|
|
1067
|
+
mode=mode_str,
|
|
1068
|
+
metadata={
|
|
1069
|
+
"adapter_type": "full",
|
|
1070
|
+
"skip_optional": self._skip_optional,
|
|
1071
|
+
"execution_order": self._execution_order,
|
|
1072
|
+
},
|
|
1073
|
+
)
|
|
1074
|
+
except Exception as e:
|
|
1075
|
+
logger.warning("Failed to save lifecycle state: %s", e)
|
|
1076
|
+
return False
|
|
1077
|
+
return False
|
|
1078
|
+
|
|
1079
|
+
def restore_state(self) -> bool:
|
|
1080
|
+
"""Restore lifecycle state from checkpoint manager."""
|
|
1081
|
+
if self._checkpoint_manager and self._task_id:
|
|
1082
|
+
try:
|
|
1083
|
+
state = self._checkpoint_manager.load_lifecycle_state(self._task_id)
|
|
1084
|
+
if state:
|
|
1085
|
+
self._current_phase = state.get("current_phase")
|
|
1086
|
+
self._completed_phases = state.get("completed_phases", [])
|
|
1087
|
+
|
|
1088
|
+
phase_states_raw = state.get("phase_states", {})
|
|
1089
|
+
self._phase_states = {}
|
|
1090
|
+
for pid, pstate in phase_states_raw.items():
|
|
1091
|
+
try:
|
|
1092
|
+
self._phase_states[pid] = PhaseState(pstate)
|
|
1093
|
+
except ValueError:
|
|
1094
|
+
self._phase_states[pid] = PhaseState.PENDING
|
|
1095
|
+
|
|
1096
|
+
mode_raw = state.get("mode", "full")
|
|
1097
|
+
try:
|
|
1098
|
+
self._mode = LifecycleMode(mode_raw)
|
|
1099
|
+
except ValueError:
|
|
1100
|
+
pass
|
|
1101
|
+
|
|
1102
|
+
# Restore metadata
|
|
1103
|
+
metadata = state.get("metadata", {})
|
|
1104
|
+
self._skip_optional = metadata.get("skip_optional", False)
|
|
1105
|
+
self._execution_order = metadata.get("execution_order", self._execution_order)
|
|
1106
|
+
|
|
1107
|
+
logger.info(
|
|
1108
|
+
"Restored lifecycle state: task=%s, phase=%s",
|
|
1109
|
+
self._task_id,
|
|
1110
|
+
self._current_phase,
|
|
1111
|
+
)
|
|
1112
|
+
return True
|
|
1113
|
+
except Exception as e:
|
|
1114
|
+
logger.warning("Failed to restore lifecycle state: %s", e)
|
|
1115
|
+
return False
|
|
1116
|
+
return False
|
|
1117
|
+
|
|
1118
|
+
def get_mode(self) -> LifecycleMode:
|
|
1119
|
+
return self._mode
|
|
1120
|
+
|
|
1121
|
+
def set_mode(self, mode: LifecycleMode) -> None:
|
|
1122
|
+
self._mode = mode
|
|
1123
|
+
|
|
1124
|
+
def get_all_phases(self) -> List[PhaseDefinition]:
|
|
1125
|
+
from scripts.collaboration.workflow_engine import PHASE_TEMPLATES
|
|
1126
|
+
|
|
1127
|
+
phases = []
|
|
1128
|
+
for pid, ptmpl in PHASE_TEMPLATES.items():
|
|
1129
|
+
phases.append(PhaseDefinition(
|
|
1130
|
+
phase_id=pid,
|
|
1131
|
+
name=ptmpl.get("name", pid),
|
|
1132
|
+
description=ptmpl.get("description", ""),
|
|
1133
|
+
role_id=ptmpl.get("role_id", ""),
|
|
1134
|
+
dependencies=ptmpl.get("dependencies", []),
|
|
1135
|
+
artifacts_in=ptmpl.get("artifacts_in", ""),
|
|
1136
|
+
artifacts_out=ptmpl.get("artifacts_out", ""),
|
|
1137
|
+
gate_condition=ptmpl.get("gate_condition", ""),
|
|
1138
|
+
reviewers=ptmpl.get("reviewers", []),
|
|
1139
|
+
optional=ptmpl.get("optional", False),
|
|
1140
|
+
))
|
|
1141
|
+
return sorted(phases, key=lambda p: p.phase_id)
|
|
1142
|
+
|
|
1143
|
+
def get_active_phases(self) -> List[PhaseDefinition]:
|
|
1144
|
+
all_phases = self.get_all_phases()
|
|
1145
|
+
if self._skip_optional:
|
|
1146
|
+
return [p for p in all_phases if not p.optional]
|
|
1147
|
+
return all_phases
|
|
1148
|
+
|
|
1149
|
+
def get_phase(self, phase_id: str) -> Optional[PhaseDefinition]:
|
|
1150
|
+
for p in self.get_all_phases():
|
|
1151
|
+
if p.phase_id == phase_id:
|
|
1152
|
+
return p
|
|
1153
|
+
return None
|
|
1154
|
+
|
|
1155
|
+
def get_current_phase(self) -> Optional[PhaseDefinition]:
|
|
1156
|
+
if self._current_phase:
|
|
1157
|
+
return self.get_phase(self._current_phase)
|
|
1158
|
+
return None
|
|
1159
|
+
|
|
1160
|
+
def advance_to_phase(self, phase_id: str) -> PhaseResult:
|
|
1161
|
+
prev_state = self._phase_states.get(phase_id, PhaseState.PENDING)
|
|
1162
|
+
|
|
1163
|
+
# Check if already completed (idempotent)
|
|
1164
|
+
if prev_state == PhaseState.COMPLETED:
|
|
1165
|
+
return PhaseResult(
|
|
1166
|
+
success=True,
|
|
1167
|
+
phase_id=phase_id,
|
|
1168
|
+
previous_state=prev_state,
|
|
1169
|
+
new_state=PhaseState.COMPLETED,
|
|
1170
|
+
gate_result=GateResult(passed=True, verdict="APPROVE"),
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
# Get phase definition
|
|
1174
|
+
phase_def = self.get_phase(phase_id)
|
|
1175
|
+
if not phase_def:
|
|
1176
|
+
result = PhaseResult(
|
|
1177
|
+
success=False,
|
|
1178
|
+
phase_id=phase_id,
|
|
1179
|
+
previous_state=prev_state,
|
|
1180
|
+
new_state=PhaseState.BLOCKED,
|
|
1181
|
+
gate_result=GateResult(
|
|
1182
|
+
passed=False,
|
|
1183
|
+
verdict="REJECT",
|
|
1184
|
+
gap_report=f"Phase {phase_id} not found in 11-phase model",
|
|
1185
|
+
),
|
|
1186
|
+
error=f"Unknown phase: {phase_id}",
|
|
1187
|
+
)
|
|
1188
|
+
self.save_state()
|
|
1189
|
+
return result
|
|
1190
|
+
|
|
1191
|
+
# Check if optional and should be skipped
|
|
1192
|
+
if self._skip_optional and phase_def.optional:
|
|
1193
|
+
self._phase_states[phase_id] = PhaseState.SKIPPED
|
|
1194
|
+
logger.info("Skipping optional phase: %s", phase_id)
|
|
1195
|
+
return PhaseResult(
|
|
1196
|
+
success=True,
|
|
1197
|
+
phase_id=phase_id,
|
|
1198
|
+
previous_state=prev_state,
|
|
1199
|
+
new_state=PhaseState.SKIPPED,
|
|
1200
|
+
gate_result=GateResult(passed=True, verdict="APPROVE"),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
# Check dependencies first (strict in FULL mode)
|
|
1204
|
+
unmet_deps = [d for d in phase_def.dependencies if d not in self._completed_phases]
|
|
1205
|
+
if unmet_deps:
|
|
1206
|
+
result = PhaseResult(
|
|
1207
|
+
success=False,
|
|
1208
|
+
phase_id=phase_id,
|
|
1209
|
+
previous_state=prev_state,
|
|
1210
|
+
new_state=PhaseState.BLOCKED,
|
|
1211
|
+
gate_result=GateResult(
|
|
1212
|
+
passed=False,
|
|
1213
|
+
verdict="CONDITIONAL",
|
|
1214
|
+
missing_evidence=[{"dependency": d} for d in unmet_deps],
|
|
1215
|
+
gap_report=f"Unmet dependencies: {', '.join(unmet_deps)}",
|
|
1216
|
+
),
|
|
1217
|
+
error=f"Unmet dependencies: {unmet_deps}",
|
|
1218
|
+
)
|
|
1219
|
+
self.save_state()
|
|
1220
|
+
return result
|
|
1221
|
+
|
|
1222
|
+
# Run gate check using unified engine or fallback
|
|
1223
|
+
gate_result = self.check_gate(phase_id)
|
|
1224
|
+
if not gate_result.passed and gate_result.verdict == "REJECT":
|
|
1225
|
+
self._phase_states[phase_id] = PhaseState.BLOCKED
|
|
1226
|
+
result = PhaseResult(
|
|
1227
|
+
success=False,
|
|
1228
|
+
phase_id=phase_id,
|
|
1229
|
+
previous_state=prev_state,
|
|
1230
|
+
new_state=PhaseState.BLOCKED,
|
|
1231
|
+
gate_result=gate_result,
|
|
1232
|
+
error=f"Gate rejected: {gate_result.gap_report}",
|
|
1233
|
+
)
|
|
1234
|
+
self.save_state()
|
|
1235
|
+
return result
|
|
1236
|
+
|
|
1237
|
+
# Advance to RUNNING state
|
|
1238
|
+
self._current_phase = phase_id
|
|
1239
|
+
self._phase_states[phase_id] = PhaseState.RUNNING
|
|
1240
|
+
logger.debug("Full mode advanced to phase %s", phase_id)
|
|
1241
|
+
self.save_state()
|
|
1242
|
+
|
|
1243
|
+
return PhaseResult(
|
|
1244
|
+
success=True,
|
|
1245
|
+
phase_id=phase_id,
|
|
1246
|
+
previous_state=prev_state,
|
|
1247
|
+
new_state=PhaseState.RUNNING,
|
|
1248
|
+
gate_result=gate_result,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
def complete_phase(self, phase_id: str) -> None:
|
|
1252
|
+
"""Mark a phase as completed."""
|
|
1253
|
+
self._phase_states[phase_id] = PhaseState.COMPLETED
|
|
1254
|
+
if phase_id not in self._completed_phases:
|
|
1255
|
+
self._completed_phases.append(phase_id)
|
|
1256
|
+
self.save_state()
|
|
1257
|
+
|
|
1258
|
+
def check_gate(self, phase_id: Optional[str] = None) -> GateResult:
|
|
1259
|
+
target = phase_id or self._current_phase
|
|
1260
|
+
if not target:
|
|
1261
|
+
return GateResult(passed=True, verdict="APPROVE")
|
|
1262
|
+
|
|
1263
|
+
phase_def = self.get_phase(target)
|
|
1264
|
+
if not phase_def:
|
|
1265
|
+
return GateResult(passed=False, verdict="REJECT", gap_report=f"Phase {target} not found")
|
|
1266
|
+
|
|
1267
|
+
# Use UnifiedGateEngine if available
|
|
1268
|
+
if self._use_unified_gate and self._gate_engine:
|
|
1269
|
+
try:
|
|
1270
|
+
return self._check_gate_with_unified_engine(target, phase_def)
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
logger.warning("UnifiedGateEngine failed, falling back: %s", e)
|
|
1273
|
+
|
|
1274
|
+
# Fallback to basic gate checks
|
|
1275
|
+
return self._check_gate_basic(target, phase_def)
|
|
1276
|
+
|
|
1277
|
+
def _check_gate_with_unified_engine(self, target: str, phase_def: PhaseDefinition) -> GateResult:
|
|
1278
|
+
"""Check gate using UnifiedGateEngine."""
|
|
1279
|
+
unmet_deps = [d for d in phase_def.dependencies if d not in self._completed_phases]
|
|
1280
|
+
|
|
1281
|
+
context = self._PhaseGateContext(
|
|
1282
|
+
phase_id=target,
|
|
1283
|
+
phase_name=phase_def.name,
|
|
1284
|
+
current_state=self._phase_states.get(target, PhaseState.PENDING).value,
|
|
1285
|
+
target_state="running",
|
|
1286
|
+
dependencies_met=len(unmet_deps) == 0,
|
|
1287
|
+
completed_phases=self._completed_phases.copy(),
|
|
1288
|
+
unmet_dependencies=unmet_deps,
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
unified_result = self._gate_engine.check(
|
|
1292
|
+
gate_type=self._GateType.PHASE_TRANSITION,
|
|
1293
|
+
context=context,
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
return GateResult(
|
|
1297
|
+
passed=unified_result.passed,
|
|
1298
|
+
verdict=unified_result.verdict,
|
|
1299
|
+
red_flags=[
|
|
1300
|
+
{"id": issue.get("code", "unknown"), "severity": "critical", **issue}
|
|
1301
|
+
for issue in unified_result.critical_issues
|
|
1302
|
+
],
|
|
1303
|
+
missing_evidence=[
|
|
1304
|
+
{"key": ev, "required": True}
|
|
1305
|
+
for ev in unified_result.evidence_required
|
|
1306
|
+
],
|
|
1307
|
+
gap_report="\n".join([
|
|
1308
|
+
f"- [{issue.get('severity', 'unknown')}] {issue.get('message', '')}"
|
|
1309
|
+
for issue in unified_result.critical_issues + unified_result.warnings
|
|
1310
|
+
]) if (unified_result.critical_issues or unified_result.warnings) else "",
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
def _check_gate_basic(self, target: str, phase_def: PhaseDefinition) -> GateResult:
|
|
1314
|
+
"""Basic fallback gate check without UnifiedGateEngine."""
|
|
1315
|
+
if not phase_def.optional:
|
|
1316
|
+
for dep in phase_def.dependencies:
|
|
1317
|
+
if dep not in self._completed_phases:
|
|
1318
|
+
dep_state = self._phase_states.get(dep, PhaseState.PENDING)
|
|
1319
|
+
if dep_state != PhaseState.COMPLETED:
|
|
1320
|
+
return GateResult(
|
|
1321
|
+
passed=False,
|
|
1322
|
+
verdict="BLOCKED",
|
|
1323
|
+
gap_report=f"Unmet dependencies: {dep}",
|
|
1324
|
+
)
|
|
1325
|
+
current_state = self._phase_states.get(target, PhaseState.PENDING)
|
|
1326
|
+
if current_state == PhaseState.BLOCKED:
|
|
1327
|
+
return GateResult(passed=False, verdict="BLOCKED", gap_report="Phase is blocked")
|
|
1328
|
+
return GateResult(passed=True, verdict="APPROVE")
|
|
1329
|
+
|
|
1330
|
+
def get_status(self) -> LifecycleStatus:
|
|
1331
|
+
all_phases = self.get_active_phases()
|
|
1332
|
+
total = len(all_phases)
|
|
1333
|
+
completed = len([p for p in all_phases if p.phase_id in self._completed_phases])
|
|
1334
|
+
progress = (completed / total * 100) if total > 0 else 0.0
|
|
1335
|
+
|
|
1336
|
+
next_phase = None
|
|
1337
|
+
can_advance = True
|
|
1338
|
+
for pid in self._execution_order:
|
|
1339
|
+
phase_def = self.get_phase(pid)
|
|
1340
|
+
if not phase_def:
|
|
1341
|
+
continue
|
|
1342
|
+
if self._skip_optional and phase_def.optional:
|
|
1343
|
+
continue
|
|
1344
|
+
state = self._phase_states.get(pid, PhaseState.PENDING)
|
|
1345
|
+
if state == PhaseState.PENDING and not next_phase:
|
|
1346
|
+
next_phase = pid
|
|
1347
|
+
if state == PhaseState.BLOCKED:
|
|
1348
|
+
can_advance = False
|
|
1349
|
+
|
|
1350
|
+
return LifecycleStatus(
|
|
1351
|
+
mode=self._mode,
|
|
1352
|
+
current_phase=self._current_phase,
|
|
1353
|
+
completed_phases=self._completed_phases.copy(),
|
|
1354
|
+
failed_phases=[pid for pid, s in self._phase_states.items() if s == PhaseState.FAILED],
|
|
1355
|
+
blocked_phases=[pid for pid, s in self._phase_states.items() if s == PhaseState.BLOCKED],
|
|
1356
|
+
progress_percent=progress,
|
|
1357
|
+
can_advance=can_advance,
|
|
1358
|
+
next_phase=next_phase,
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
def get_view_mapping(self, command: str) -> Optional[ViewMapping]:
|
|
1362
|
+
return VIEW_MAPPINGS.get(command)
|
|
1363
|
+
|
|
1364
|
+
def resolve_command_to_phases(self, command: str) -> List[PhaseDefinition]:
|
|
1365
|
+
mapping = VIEW_MAPPINGS.get(command)
|
|
1366
|
+
if not mapping:
|
|
1367
|
+
return []
|
|
1368
|
+
return [p for p in self.get_all_phases() if mapping.covers_phase(p.phase_id)]
|
|
1369
|
+
|
|
1370
|
+
def get_next_phase(self) -> Optional[str]:
|
|
1371
|
+
"""Get the next phase that should be executed based on execution order."""
|
|
1372
|
+
for pid in self._execution_order:
|
|
1373
|
+
state = self._phase_states.get(pid, PhaseState.PENDING)
|
|
1374
|
+
if state == PhaseState.PENDING:
|
|
1375
|
+
phase_def = self.get_phase(pid)
|
|
1376
|
+
if phase_def and (not self._skip_optional or not phase_def.optional):
|
|
1377
|
+
return pid
|
|
1378
|
+
return None
|
|
1379
|
+
|
|
1380
|
+
def auto_advance(self) -> PhaseResult:
|
|
1381
|
+
"""Automatically advance to the next pending phase."""
|
|
1382
|
+
next_phase = self.get_next_phase()
|
|
1383
|
+
if not next_phase:
|
|
1384
|
+
return PhaseResult(
|
|
1385
|
+
success=False,
|
|
1386
|
+
phase_id="",
|
|
1387
|
+
previous_state=PhaseState.PENDING,
|
|
1388
|
+
new_state=PhaseState.PENDING,
|
|
1389
|
+
error="No more phases to execute",
|
|
1390
|
+
)
|
|
1391
|
+
return self.advance_to_phase(next_phase)
|
|
1392
|
+
|
|
1393
|
+
def get_execution_progress(self) -> Dict[str, Any]:
|
|
1394
|
+
"""Get detailed execution progress information."""
|
|
1395
|
+
phases_info = []
|
|
1396
|
+
for pid in self._execution_order:
|
|
1397
|
+
phase_def = self.get_phase(pid)
|
|
1398
|
+
if not phase_def:
|
|
1399
|
+
continue
|
|
1400
|
+
state = self._phase_states.get(pid, PhaseState.PENDING)
|
|
1401
|
+
phases_info.append({
|
|
1402
|
+
"phase_id": pid,
|
|
1403
|
+
"name": phase_def.name,
|
|
1404
|
+
"state": state.value if hasattr(state, 'value') else str(state),
|
|
1405
|
+
"optional": phase_def.optional,
|
|
1406
|
+
"role": phase_def.role_id,
|
|
1407
|
+
"completed": pid in self._completed_phases,
|
|
1408
|
+
})
|
|
1409
|
+
|
|
1410
|
+
status = self.get_status()
|
|
1411
|
+
return {
|
|
1412
|
+
"total_phases": len(self.get_active_phases()),
|
|
1413
|
+
"completed_phases": len(status.completed_phases),
|
|
1414
|
+
"progress_percent": status.progress_percent,
|
|
1415
|
+
"current_phase": status.current_phase,
|
|
1416
|
+
"next_phase": status.next_phase,
|
|
1417
|
+
"can_advance": status.can_advance,
|
|
1418
|
+
"phases": phases_info,
|
|
1419
|
+
"execution_order": self._execution_order,
|
|
1420
|
+
"skip_optional": self._skip_optional,
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
def init_spec(
|
|
1424
|
+
self,
|
|
1425
|
+
template_id: str = "requirements",
|
|
1426
|
+
project_info: Optional[Dict[str, Any]] = None,
|
|
1427
|
+
) -> Dict[str, Any]:
|
|
1428
|
+
shortcut = ShortcutLifecycleAdapter(use_unified_gate=self._use_unified_gate)
|
|
1429
|
+
return shortcut.init_spec(template_id, project_info)
|
|
1430
|
+
|
|
1431
|
+
def analyze_spec(
|
|
1432
|
+
self,
|
|
1433
|
+
source_dir: str = ".",
|
|
1434
|
+
template_id: str = "requirements",
|
|
1435
|
+
) -> Dict[str, Any]:
|
|
1436
|
+
shortcut = ShortcutLifecycleAdapter(use_unified_gate=self._use_unified_gate)
|
|
1437
|
+
return shortcut.analyze_spec(source_dir, template_id)
|
|
1438
|
+
|
|
1439
|
+
def validate_spec(
|
|
1440
|
+
self,
|
|
1441
|
+
spec_path: Optional[str] = None,
|
|
1442
|
+
template_id: Optional[str] = None,
|
|
1443
|
+
spec_data: Optional[Dict[str, Any]] = None,
|
|
1444
|
+
) -> Dict[str, Any]:
|
|
1445
|
+
shortcut = ShortcutLifecycleAdapter(use_unified_gate=self._use_unified_gate)
|
|
1446
|
+
return shortcut.validate_spec(spec_path, template_id, spec_data)
|