devforgeai 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +120 -0
- package/bin/devforgeai.js +0 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/hooks/phase-completion-gate.sh +0 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevForgeAI Orchestrate Hooks - Workflow Context Extraction
|
|
3
|
+
|
|
4
|
+
Extracts comprehensive workflow context for /orchestrate command hooks.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Workflow status determination (SUCCESS/FAILURE)
|
|
8
|
+
- Phase execution tracking (dev, qa, release)
|
|
9
|
+
- Quality gate aggregation
|
|
10
|
+
- Checkpoint resume detection
|
|
11
|
+
- Failure reason extraction
|
|
12
|
+
- Context validation and JSON serialization
|
|
13
|
+
|
|
14
|
+
Story: STORY-026 - Wire hooks into /orchestrate command
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
from typing import Dict, List, Any, Optional
|
|
22
|
+
from uuid import uuid4
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Module-Level Constants
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
# Phase identifiers and labels
|
|
31
|
+
PHASE_DEVELOPMENT = "development"
|
|
32
|
+
PHASE_QA = "qa"
|
|
33
|
+
PHASE_RELEASE = "release"
|
|
34
|
+
PHASE_LABELS = {
|
|
35
|
+
PHASE_DEVELOPMENT: "Development",
|
|
36
|
+
PHASE_QA: "QA",
|
|
37
|
+
PHASE_RELEASE: "Release",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Workflow status values
|
|
41
|
+
STATUS_PASSED = "PASSED"
|
|
42
|
+
STATUS_FAILED = "FAILED"
|
|
43
|
+
STATUS_NOT_RUN = "NOT_RUN"
|
|
44
|
+
VALID_STATUSES = {STATUS_PASSED, STATUS_FAILED, STATUS_NOT_RUN}
|
|
45
|
+
|
|
46
|
+
# Workflow completion statuses
|
|
47
|
+
WORKFLOW_SUCCESS = "SUCCESS"
|
|
48
|
+
WORKFLOW_FAILURE = "FAILURE"
|
|
49
|
+
|
|
50
|
+
# Quality gate identifiers
|
|
51
|
+
GATE_CONTEXT_VALIDATION = "context_validation"
|
|
52
|
+
GATE_TEST_PASSING = "test_passing"
|
|
53
|
+
GATE_COVERAGE = "coverage"
|
|
54
|
+
GATE_QA_APPROVED = "qa_approved"
|
|
55
|
+
|
|
56
|
+
# Regex patterns
|
|
57
|
+
PATTERN_TIMESTAMP_ISO8601 = r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"
|
|
58
|
+
PATTERN_DURATION = r"Duration:\s*(\d+)"
|
|
59
|
+
PATTERN_QA_ATTEMPT = r"### QA Attempt (\d+)"
|
|
60
|
+
PATTERN_STATUS_LINE = r"Status:\s*(\w+)"
|
|
61
|
+
PATTERN_STATUS_BULLET = r"-\s*Status:\s*(\w+)"
|
|
62
|
+
PATTERN_CHECKPOINT = r"Checkpoint:\s*(\w+)"
|
|
63
|
+
PATTERN_PREVIOUS_DURATION = r"previous_duration:\s*(\d+)"
|
|
64
|
+
PATTERN_QA_ATTEMPTS_FIELD = r"qa_attempts:\s*(\d+)"
|
|
65
|
+
PATTERN_FAILED_CRITERION = r"Failed Criterion:\s*(.+?)$"
|
|
66
|
+
PATTERN_FAILURE_REASON = r"failure_reason:\s*(.+?)$"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OrchestrateHooksContextExtractor:
|
|
70
|
+
"""
|
|
71
|
+
Extracts comprehensive workflow context from orchestrate operations.
|
|
72
|
+
|
|
73
|
+
This extractor parses story files to determine workflow status, phase
|
|
74
|
+
execution, quality gates, and failure information. All extracted data
|
|
75
|
+
is validated and returned in a consistent JSON-serializable format.
|
|
76
|
+
|
|
77
|
+
Typical usage:
|
|
78
|
+
extractor = OrchestrateHooksContextExtractor()
|
|
79
|
+
context = extractor.extract_workflow_context(story_content, "STORY-001")
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
"""Initialize the context extractor (no state required)."""
|
|
84
|
+
|
|
85
|
+
def extract_workflow_context(
|
|
86
|
+
self,
|
|
87
|
+
story_content: str,
|
|
88
|
+
story_id: str,
|
|
89
|
+
workflow_start_time: Optional[str] = None,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Extract complete workflow context from story file.
|
|
93
|
+
|
|
94
|
+
Parses story file to determine:
|
|
95
|
+
- Phase execution status (dev, qa, release)
|
|
96
|
+
- Workflow completion status (SUCCESS/FAILURE)
|
|
97
|
+
- Quality gate status and failure reasons
|
|
98
|
+
- Checkpoint resume information
|
|
99
|
+
- Workflow timing and durations
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
story_content: Complete story file content
|
|
103
|
+
story_id: Story ID (e.g., STORY-001)
|
|
104
|
+
workflow_start_time: Optional ISO8601 start time
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary with keys: workflow_id, story_id, status, total_duration,
|
|
108
|
+
start_time, end_time, phases_executed, quality_gates, checkpoint_info.
|
|
109
|
+
If status is FAILURE, also includes: failed_phase, failure_summary,
|
|
110
|
+
phases_aborted, and optionally qa_attempts.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
No exceptions raised; errors logged and error context returned.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
phases_executed = self._extract_phases(story_content)
|
|
117
|
+
status = self._determine_status(phases_executed)
|
|
118
|
+
quality_gates = self._extract_quality_gates(story_content, phases_executed)
|
|
119
|
+
checkpoint_info = self._extract_checkpoint_info(story_content, phases_executed)
|
|
120
|
+
start_time, end_time, duration = self._calculate_duration(
|
|
121
|
+
story_content, phases_executed, workflow_start_time
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
context = self._build_context(
|
|
125
|
+
story_id=story_id,
|
|
126
|
+
status=status,
|
|
127
|
+
duration=duration,
|
|
128
|
+
start_time=start_time,
|
|
129
|
+
end_time=end_time,
|
|
130
|
+
phases_executed=phases_executed,
|
|
131
|
+
quality_gates=quality_gates,
|
|
132
|
+
checkpoint_info=checkpoint_info,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Add failure-specific fields if workflow failed
|
|
136
|
+
if status == WORKFLOW_FAILURE:
|
|
137
|
+
self._add_failure_context(context, story_content, phases_executed)
|
|
138
|
+
|
|
139
|
+
return context
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Context extraction error: {str(e)}")
|
|
143
|
+
return self._create_error_context(story_id)
|
|
144
|
+
|
|
145
|
+
def _build_context(
|
|
146
|
+
self,
|
|
147
|
+
story_id: str,
|
|
148
|
+
status: str,
|
|
149
|
+
duration: int,
|
|
150
|
+
start_time: str,
|
|
151
|
+
end_time: str,
|
|
152
|
+
phases_executed: List[Dict[str, Any]],
|
|
153
|
+
quality_gates: Dict[str, Any],
|
|
154
|
+
checkpoint_info: Dict[str, Any],
|
|
155
|
+
) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Build base workflow context dictionary.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
story_id: Story identifier
|
|
161
|
+
status: Workflow status (SUCCESS/FAILURE)
|
|
162
|
+
duration: Total workflow duration in seconds
|
|
163
|
+
start_time: ISO8601 start timestamp
|
|
164
|
+
end_time: ISO8601 end timestamp
|
|
165
|
+
phases_executed: List of phase execution data
|
|
166
|
+
quality_gates: Quality gate status dictionary
|
|
167
|
+
checkpoint_info: Checkpoint resume information
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Context dictionary with all provided fields
|
|
171
|
+
"""
|
|
172
|
+
return {
|
|
173
|
+
"workflow_id": str(uuid4()),
|
|
174
|
+
"story_id": story_id,
|
|
175
|
+
"status": status,
|
|
176
|
+
"total_duration": duration,
|
|
177
|
+
"start_time": start_time,
|
|
178
|
+
"end_time": end_time,
|
|
179
|
+
"phases_executed": phases_executed,
|
|
180
|
+
"quality_gates": quality_gates,
|
|
181
|
+
"checkpoint_info": checkpoint_info,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def _add_failure_context(
|
|
185
|
+
self,
|
|
186
|
+
context: Dict[str, Any],
|
|
187
|
+
story_content: str,
|
|
188
|
+
phases_executed: List[Dict[str, Any]],
|
|
189
|
+
) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Add failure-specific information to context (mutates context dict).
|
|
192
|
+
|
|
193
|
+
Determines which phase failed, extracts failure reasons, identifies
|
|
194
|
+
aborted phases, and counts QA attempts.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
context: Context dictionary to update with failure info
|
|
198
|
+
story_content: Story file content for parsing
|
|
199
|
+
phases_executed: List of phase execution data
|
|
200
|
+
"""
|
|
201
|
+
failed_phase = self._get_failed_phase(phases_executed)
|
|
202
|
+
context["failed_phase"] = failed_phase
|
|
203
|
+
context["failure_summary"] = self._extract_failure_summary(
|
|
204
|
+
story_content, failed_phase
|
|
205
|
+
)
|
|
206
|
+
context["phases_aborted"] = self._get_aborted_phases(phases_executed)
|
|
207
|
+
|
|
208
|
+
qa_attempts = self._extract_qa_attempts(story_content)
|
|
209
|
+
if qa_attempts is not None:
|
|
210
|
+
context["qa_attempts"] = qa_attempts
|
|
211
|
+
|
|
212
|
+
def _extract_phases(self, story_content: str) -> List[Dict[str, Any]]:
|
|
213
|
+
"""
|
|
214
|
+
Extract phase information from workflow history.
|
|
215
|
+
|
|
216
|
+
Parses story content to find development, QA, and release phases.
|
|
217
|
+
Returns phases in execution order.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
story_content: Story content to parse
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of phase dictionaries with status and metadata
|
|
224
|
+
"""
|
|
225
|
+
phases = []
|
|
226
|
+
|
|
227
|
+
# Extract each phase in order
|
|
228
|
+
for phase_key, phase_label in PHASE_LABELS.items():
|
|
229
|
+
phase_data = self._extract_phase(story_content, phase_key, phase_label)
|
|
230
|
+
if phase_data:
|
|
231
|
+
phases.append(phase_data)
|
|
232
|
+
|
|
233
|
+
return phases
|
|
234
|
+
|
|
235
|
+
def _extract_phase(
|
|
236
|
+
self, content: str, phase_key: str, phase_label: str
|
|
237
|
+
) -> Optional[Dict[str, Any]]:
|
|
238
|
+
"""
|
|
239
|
+
Extract specific phase information from story content.
|
|
240
|
+
|
|
241
|
+
Searches for phase section, extracts status and duration, and adds
|
|
242
|
+
phase-specific metadata (qa_attempts for QA phase).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
content: Story content to search
|
|
246
|
+
phase_key: Phase identifier (development, qa, release)
|
|
247
|
+
phase_label: Phase label as it appears in content
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Phase dictionary with keys: phase, status, duration (if available),
|
|
251
|
+
and phase-specific fields. Returns None if phase not found.
|
|
252
|
+
"""
|
|
253
|
+
phase_pattern = rf"### {phase_label}.*?\n(.*?)(?=###|$)"
|
|
254
|
+
match = re.search(phase_pattern, content, re.IGNORECASE | re.DOTALL)
|
|
255
|
+
|
|
256
|
+
if not match:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
phase_content = match.group(1)
|
|
260
|
+
status = self._extract_status(phase_content)
|
|
261
|
+
|
|
262
|
+
if not status:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
phase_data: Dict[str, Any] = {
|
|
266
|
+
"phase": phase_key,
|
|
267
|
+
"status": status,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Extract optional duration
|
|
271
|
+
duration = self._extract_duration_from_phase(phase_content)
|
|
272
|
+
if duration:
|
|
273
|
+
phase_data["duration"] = duration
|
|
274
|
+
|
|
275
|
+
# Extract phase-specific details
|
|
276
|
+
if phase_key == PHASE_QA:
|
|
277
|
+
self._add_qa_phase_details(phase_data, phase_content)
|
|
278
|
+
|
|
279
|
+
return phase_data
|
|
280
|
+
|
|
281
|
+
def _add_qa_phase_details(self, phase_data: Dict[str, Any], content: str) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Add QA-specific phase details to phase data (mutates phase_data dict).
|
|
284
|
+
|
|
285
|
+
Extracts QA attempt count and failure reason from content.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
phase_data: Phase dictionary to update
|
|
289
|
+
content: Phase content to parse
|
|
290
|
+
"""
|
|
291
|
+
qa_attempts = self._extract_qa_attempt_count(content)
|
|
292
|
+
if qa_attempts:
|
|
293
|
+
phase_data["qa_attempts"] = qa_attempts
|
|
294
|
+
|
|
295
|
+
failure_reason = self._extract_failure_reason(content)
|
|
296
|
+
if failure_reason:
|
|
297
|
+
phase_data["failure_reason"] = failure_reason
|
|
298
|
+
|
|
299
|
+
def _extract_status(self, content: str) -> Optional[str]:
|
|
300
|
+
"""
|
|
301
|
+
Extract status from phase content.
|
|
302
|
+
|
|
303
|
+
Checks for status in "Status: VALUE" or "- Status: VALUE" format.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
content: Phase content to search
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Status string (PASSED, FAILED, NOT_RUN) or None if not found
|
|
310
|
+
"""
|
|
311
|
+
# Try pattern with leading status line
|
|
312
|
+
match = re.search(PATTERN_STATUS_LINE, content)
|
|
313
|
+
if match:
|
|
314
|
+
status = match.group(1).upper()
|
|
315
|
+
if status in VALID_STATUSES:
|
|
316
|
+
return status
|
|
317
|
+
|
|
318
|
+
# Try pattern with bullet point "- Status: VALUE"
|
|
319
|
+
match = re.search(PATTERN_STATUS_BULLET, content)
|
|
320
|
+
if match:
|
|
321
|
+
status = match.group(1).upper()
|
|
322
|
+
if status in VALID_STATUSES:
|
|
323
|
+
return status
|
|
324
|
+
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def _extract_duration_from_phase(self, content: str) -> Optional[int]:
|
|
328
|
+
"""
|
|
329
|
+
Extract duration in seconds from phase content.
|
|
330
|
+
|
|
331
|
+
Searches for "Duration: N" pattern where N is a number of seconds.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
content: Phase content to search
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Duration in seconds or None if not found
|
|
338
|
+
"""
|
|
339
|
+
match = re.search(PATTERN_DURATION, content)
|
|
340
|
+
if match:
|
|
341
|
+
return int(match.group(1))
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def _extract_qa_attempt_count(self, content: str) -> Optional[int]:
|
|
345
|
+
"""
|
|
346
|
+
Extract QA attempt count from phase content.
|
|
347
|
+
|
|
348
|
+
Counts "### QA Attempt N" sections or looks for qa_attempts field.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
content: Phase content to search
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Number of QA attempts or None if not found
|
|
355
|
+
"""
|
|
356
|
+
# Count "### QA Attempt N" sections
|
|
357
|
+
attempts = re.findall(PATTERN_QA_ATTEMPT, content)
|
|
358
|
+
if attempts:
|
|
359
|
+
return len(attempts)
|
|
360
|
+
|
|
361
|
+
# Also check for qa_attempts field
|
|
362
|
+
match = re.search(PATTERN_QA_ATTEMPTS_FIELD, content)
|
|
363
|
+
if match:
|
|
364
|
+
return int(match.group(1))
|
|
365
|
+
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
def _extract_failure_reason(self, content: str) -> Optional[str]:
|
|
369
|
+
"""
|
|
370
|
+
Extract failure reason from phase content.
|
|
371
|
+
|
|
372
|
+
Searches for "Failed Criterion:" or "failure_reason:" patterns.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
content: Phase content to search
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Failure reason string or None if not found
|
|
379
|
+
"""
|
|
380
|
+
match = re.search(PATTERN_FAILED_CRITERION, content, re.MULTILINE)
|
|
381
|
+
if match:
|
|
382
|
+
return match.group(1).strip()
|
|
383
|
+
|
|
384
|
+
match = re.search(PATTERN_FAILURE_REASON, content, re.MULTILINE)
|
|
385
|
+
if match:
|
|
386
|
+
return match.group(1).strip()
|
|
387
|
+
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
def _determine_status(self, phases: List[Dict[str, Any]]) -> str:
|
|
391
|
+
"""
|
|
392
|
+
Determine overall workflow status based on phase statuses.
|
|
393
|
+
|
|
394
|
+
All phases must have PASSED status for workflow to be SUCCESS.
|
|
395
|
+
If any phase is FAILED or NOT_RUN, workflow is FAILURE.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
phases: List of phase dictionaries with status field
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Workflow completion status (SUCCESS or FAILURE)
|
|
402
|
+
"""
|
|
403
|
+
if not phases:
|
|
404
|
+
return WORKFLOW_FAILURE
|
|
405
|
+
|
|
406
|
+
# All phases must be PASSED for SUCCESS
|
|
407
|
+
for phase in phases:
|
|
408
|
+
if phase.get("status") != STATUS_PASSED:
|
|
409
|
+
return WORKFLOW_FAILURE
|
|
410
|
+
|
|
411
|
+
return WORKFLOW_SUCCESS
|
|
412
|
+
|
|
413
|
+
def _extract_quality_gates(
|
|
414
|
+
self, content: str, phases: List[Dict[str, Any]]
|
|
415
|
+
) -> Dict[str, Any]:
|
|
416
|
+
"""
|
|
417
|
+
Extract quality gate information from phase data.
|
|
418
|
+
|
|
419
|
+
Initializes all gates with PASSED status, then marks failed gates
|
|
420
|
+
based on phase failure information.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
content: Story content (reserved for future use)
|
|
424
|
+
phases: List of phase dictionaries with status and metadata
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Dictionary with quality gate status for each gate
|
|
428
|
+
"""
|
|
429
|
+
gates = self._initialize_quality_gates()
|
|
430
|
+
|
|
431
|
+
# Check if any phase failed and update gates accordingly
|
|
432
|
+
for phase in phases:
|
|
433
|
+
if phase.get("status") == STATUS_FAILED:
|
|
434
|
+
self._update_failed_gates(gates, phase)
|
|
435
|
+
|
|
436
|
+
return gates
|
|
437
|
+
|
|
438
|
+
def _initialize_quality_gates(self) -> Dict[str, Any]:
|
|
439
|
+
"""
|
|
440
|
+
Initialize all quality gates with PASSED status.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Dictionary with all quality gates initialized to PASSED
|
|
444
|
+
"""
|
|
445
|
+
return {
|
|
446
|
+
GATE_CONTEXT_VALIDATION: {"status": STATUS_PASSED},
|
|
447
|
+
GATE_TEST_PASSING: {"status": STATUS_PASSED},
|
|
448
|
+
GATE_COVERAGE: {"status": STATUS_PASSED},
|
|
449
|
+
GATE_QA_APPROVED: {"status": STATUS_PASSED},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
def _update_failed_gates(self, gates: Dict[str, Any], phase: Dict[str, Any]) -> None:
|
|
453
|
+
"""
|
|
454
|
+
Update gate statuses based on phase failure (mutates gates dict).
|
|
455
|
+
|
|
456
|
+
If the failed phase is QA, marks QA gate as FAILED and includes
|
|
457
|
+
failure reason if available.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
gates: Quality gates dictionary to update
|
|
461
|
+
phase: Phase data with failure information
|
|
462
|
+
"""
|
|
463
|
+
if phase.get("phase") == PHASE_QA:
|
|
464
|
+
gates[GATE_QA_APPROVED]["status"] = STATUS_FAILED
|
|
465
|
+
if "failure_reason" in phase:
|
|
466
|
+
gates[GATE_QA_APPROVED]["reason"] = phase["failure_reason"]
|
|
467
|
+
|
|
468
|
+
def _extract_checkpoint_info(
|
|
469
|
+
self, content: str, phases: List[Dict[str, Any]]
|
|
470
|
+
) -> Dict[str, Any]:
|
|
471
|
+
"""
|
|
472
|
+
Extract checkpoint/resume information from story content.
|
|
473
|
+
|
|
474
|
+
Detects checkpoint resume patterns and extracts resume point,
|
|
475
|
+
previous phase execution, and cumulative durations.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
content: Story content to search
|
|
479
|
+
phases: List of phase dictionaries
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Checkpoint info dictionary with resume status and details
|
|
483
|
+
"""
|
|
484
|
+
checkpoint_info: Dict[str, Any] = {
|
|
485
|
+
"checkpoint_resumed": False,
|
|
486
|
+
"resume_point": None,
|
|
487
|
+
"phases_skipped": [],
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# Check if checkpoint/resume pattern exists
|
|
491
|
+
if "Checkpoint:" not in content or "Resume" not in content:
|
|
492
|
+
return checkpoint_info
|
|
493
|
+
|
|
494
|
+
checkpoint_info["checkpoint_resumed"] = True
|
|
495
|
+
|
|
496
|
+
# Extract resume point (e.g., QA_APPROVED)
|
|
497
|
+
match = re.search(PATTERN_CHECKPOINT, content)
|
|
498
|
+
if match:
|
|
499
|
+
checkpoint_info["resume_point"] = match.group(1)
|
|
500
|
+
|
|
501
|
+
# Track phases in previous sessions
|
|
502
|
+
phases_in_previous = self._extract_previous_phases(content, phases)
|
|
503
|
+
if phases_in_previous:
|
|
504
|
+
checkpoint_info["phases_in_previous_sessions"] = phases_in_previous
|
|
505
|
+
|
|
506
|
+
# Extract previous duration if available
|
|
507
|
+
match = re.search(PATTERN_PREVIOUS_DURATION, content)
|
|
508
|
+
if match:
|
|
509
|
+
checkpoint_info["previous_phases_duration"] = int(match.group(1))
|
|
510
|
+
|
|
511
|
+
return checkpoint_info
|
|
512
|
+
|
|
513
|
+
def _extract_previous_phases(
|
|
514
|
+
self, content: str, phases: List[Dict[str, Any]]
|
|
515
|
+
) -> List[Dict[str, str]]:
|
|
516
|
+
"""
|
|
517
|
+
Extract phases that ran in previous checkpoint sessions.
|
|
518
|
+
|
|
519
|
+
Identifies phases marked as "previous" in content.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
content: Story content to search
|
|
523
|
+
phases: List of all phase dictionaries
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
List of phase data from previous sessions
|
|
527
|
+
"""
|
|
528
|
+
phases_in_previous = []
|
|
529
|
+
|
|
530
|
+
for phase in phases:
|
|
531
|
+
# Check if phase is marked as from previous session
|
|
532
|
+
if "previous" in content.lower() and phase.get("phase") in content:
|
|
533
|
+
phases_in_previous.append({
|
|
534
|
+
"phase": phase.get("phase"),
|
|
535
|
+
"status": phase.get("status"),
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
return phases_in_previous
|
|
539
|
+
|
|
540
|
+
def _calculate_duration(
|
|
541
|
+
self,
|
|
542
|
+
content: str,
|
|
543
|
+
phases: List[Dict[str, Any]],
|
|
544
|
+
workflow_start_time: Optional[str] = None,
|
|
545
|
+
) -> tuple:
|
|
546
|
+
"""
|
|
547
|
+
Calculate workflow timing information.
|
|
548
|
+
|
|
549
|
+
Sums phase durations and extracts/generates start and end times.
|
|
550
|
+
If workflow_start_time provided, uses that as start. If end time
|
|
551
|
+
not found, calculates from start time + total duration.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
content: Story content to search for timestamps
|
|
555
|
+
phases: List of phase dictionaries with duration field
|
|
556
|
+
workflow_start_time: Optional ISO8601 start time
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Tuple of (start_time, end_time, total_duration_seconds)
|
|
560
|
+
"""
|
|
561
|
+
# Calculate total duration from phase durations
|
|
562
|
+
total_duration = sum(p.get("duration", 0) for p in phases)
|
|
563
|
+
|
|
564
|
+
# Extract timestamps from content
|
|
565
|
+
start_time_str, end_time_str = self._extract_timestamps_from_content(content)
|
|
566
|
+
|
|
567
|
+
# Override start time if provided
|
|
568
|
+
if workflow_start_time:
|
|
569
|
+
start_time_str = workflow_start_time
|
|
570
|
+
|
|
571
|
+
# Generate end time if not found
|
|
572
|
+
if not end_time_str:
|
|
573
|
+
end_time_str = self._calculate_end_time(start_time_str, total_duration)
|
|
574
|
+
|
|
575
|
+
# Ensure both times are set
|
|
576
|
+
start_time_str = start_time_str or datetime.utcnow().isoformat() + "Z"
|
|
577
|
+
end_time_str = end_time_str or datetime.utcnow().isoformat() + "Z"
|
|
578
|
+
|
|
579
|
+
return start_time_str, end_time_str, total_duration
|
|
580
|
+
|
|
581
|
+
def _extract_timestamps_from_content(self, content: str) -> tuple:
|
|
582
|
+
"""
|
|
583
|
+
Extract ISO8601 timestamps from story content.
|
|
584
|
+
|
|
585
|
+
Searches for first and last ISO8601 timestamp in content.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
content: Story content to search
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Tuple of (start_timestamp, end_timestamp) or (None, None)
|
|
592
|
+
"""
|
|
593
|
+
times = re.findall(PATTERN_TIMESTAMP_ISO8601, content)
|
|
594
|
+
|
|
595
|
+
start_time_str = times[0] if times else None
|
|
596
|
+
end_time_str = times[-1] if len(times) > 1 else None
|
|
597
|
+
|
|
598
|
+
return start_time_str, end_time_str
|
|
599
|
+
|
|
600
|
+
def _calculate_end_time(self, start_time_str: Optional[str], duration: int) -> str:
|
|
601
|
+
"""
|
|
602
|
+
Calculate end time from start time and duration.
|
|
603
|
+
|
|
604
|
+
Parses ISO8601 start time, adds duration in seconds, returns
|
|
605
|
+
resulting ISO8601 timestamp. Falls back to current time on error.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
start_time_str: ISO8601 start timestamp or None
|
|
609
|
+
duration: Duration in seconds to add
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
ISO8601 end timestamp
|
|
613
|
+
"""
|
|
614
|
+
if not start_time_str:
|
|
615
|
+
return datetime.utcnow().isoformat() + "Z"
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
start_dt = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
|
|
619
|
+
end_dt = start_dt + timedelta(seconds=duration)
|
|
620
|
+
return end_dt.isoformat().replace("+00:00", "Z")
|
|
621
|
+
except (ValueError, OverflowError):
|
|
622
|
+
return datetime.utcnow().isoformat() + "Z"
|
|
623
|
+
|
|
624
|
+
def _get_failed_phase(self, phases: List[Dict[str, Any]]) -> Optional[str]:
|
|
625
|
+
"""
|
|
626
|
+
Get the first failed phase.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
phases: List of phase dictionaries
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Phase identifier (development, qa, release) or None
|
|
633
|
+
"""
|
|
634
|
+
for phase in phases:
|
|
635
|
+
if phase.get("status") == STATUS_FAILED:
|
|
636
|
+
return phase.get("phase")
|
|
637
|
+
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
def _get_aborted_phases(self, phases: List[Dict[str, Any]]) -> List[str]:
|
|
641
|
+
"""
|
|
642
|
+
Get phases that were aborted (NOT_RUN after failure).
|
|
643
|
+
|
|
644
|
+
Phases are aborted when a previous phase fails and stops workflow.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
phases: List of phase dictionaries
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
List of aborted phase identifiers
|
|
651
|
+
"""
|
|
652
|
+
aborted = []
|
|
653
|
+
found_failure = False
|
|
654
|
+
|
|
655
|
+
for phase in phases:
|
|
656
|
+
if phase.get("status") == STATUS_FAILED:
|
|
657
|
+
found_failure = True
|
|
658
|
+
elif found_failure and phase.get("status") == STATUS_NOT_RUN:
|
|
659
|
+
aborted.append(phase.get("phase"))
|
|
660
|
+
|
|
661
|
+
return aborted
|
|
662
|
+
|
|
663
|
+
def _extract_failure_summary(self, content: str, failed_phase: Optional[str]) -> str:
|
|
664
|
+
"""
|
|
665
|
+
Extract failure summary from story content.
|
|
666
|
+
|
|
667
|
+
Generates a human-readable failure message for the failed phase.
|
|
668
|
+
Attempts to extract specific failure reason from content, falls back
|
|
669
|
+
to generic phase failure message.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
content: Story content to search for failure details
|
|
673
|
+
failed_phase: Phase identifier that failed (or None)
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
Failure summary message string
|
|
677
|
+
"""
|
|
678
|
+
if not failed_phase:
|
|
679
|
+
return "Unknown failure"
|
|
680
|
+
|
|
681
|
+
# Define phase-specific failure patterns and messages
|
|
682
|
+
phase_configs = {
|
|
683
|
+
PHASE_QA: ("QA.*?Failed.*?:\s*(.+?)(?:\n|$)", "QA validation failed"),
|
|
684
|
+
PHASE_RELEASE: ("Release.*?Failed.*?:\s*(.+?)(?:\n|$)", "Release failed"),
|
|
685
|
+
PHASE_DEVELOPMENT: (
|
|
686
|
+
"Development.*?Failed.*?:\s*(.+?)(?:\n|$)",
|
|
687
|
+
"Development failed",
|
|
688
|
+
),
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if failed_phase in phase_configs:
|
|
692
|
+
pattern, default_msg = phase_configs[failed_phase]
|
|
693
|
+
match = re.search(pattern, content)
|
|
694
|
+
if match:
|
|
695
|
+
return f"{default_msg}: {match.group(1)}"
|
|
696
|
+
return default_msg
|
|
697
|
+
|
|
698
|
+
return f"{failed_phase} phase failed"
|
|
699
|
+
|
|
700
|
+
def _extract_qa_attempts(self, content: str) -> Optional[int]:
|
|
701
|
+
"""
|
|
702
|
+
Extract total QA attempt count from story content.
|
|
703
|
+
|
|
704
|
+
Counts "### QA Attempt N" sections or looks for qa_attempts field.
|
|
705
|
+
This is a top-level count across all QA phases in story history.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
content: Story content to search
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Total number of QA attempts or None if not found
|
|
712
|
+
"""
|
|
713
|
+
# Count "### QA Attempt N" sections at top level
|
|
714
|
+
attempts = re.findall(PATTERN_QA_ATTEMPT, content)
|
|
715
|
+
if attempts:
|
|
716
|
+
return len(attempts)
|
|
717
|
+
|
|
718
|
+
# Also check for qa_attempts field at story level
|
|
719
|
+
match = re.search(PATTERN_QA_ATTEMPTS_FIELD, content)
|
|
720
|
+
if match:
|
|
721
|
+
return int(match.group(1))
|
|
722
|
+
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
def _create_error_context(self, story_id: str) -> Dict[str, Any]:
|
|
726
|
+
"""
|
|
727
|
+
Create minimal error context for extraction failures.
|
|
728
|
+
|
|
729
|
+
Returns a consistent error structure with failure status and
|
|
730
|
+
no phase data. Used when context extraction encounters an exception.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
story_id: Story identifier
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
Minimal context with FAILURE status and empty phases
|
|
737
|
+
"""
|
|
738
|
+
return {
|
|
739
|
+
"workflow_id": str(uuid4()),
|
|
740
|
+
"story_id": story_id,
|
|
741
|
+
"status": WORKFLOW_FAILURE,
|
|
742
|
+
"total_duration": 0,
|
|
743
|
+
"start_time": datetime.utcnow().isoformat() + "Z",
|
|
744
|
+
"end_time": datetime.utcnow().isoformat() + "Z",
|
|
745
|
+
"phases_executed": [],
|
|
746
|
+
"quality_gates": self._initialize_quality_gates(),
|
|
747
|
+
"checkpoint_info": {"checkpoint_resumed": False},
|
|
748
|
+
"failure_summary": "Context extraction failed",
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def extract_orchestrate_context(
|
|
753
|
+
story_content: str,
|
|
754
|
+
story_id: str,
|
|
755
|
+
workflow_start_time: Optional[str] = None,
|
|
756
|
+
) -> Dict[str, Any]:
|
|
757
|
+
"""
|
|
758
|
+
Public API for extracting orchestrate workflow context.
|
|
759
|
+
|
|
760
|
+
Main entry point for extracting comprehensive workflow context from
|
|
761
|
+
story files. Instantiates extractor and delegates to extract_workflow_context.
|
|
762
|
+
|
|
763
|
+
Example:
|
|
764
|
+
story_content = Path("STORY-001.story.md").read_text()
|
|
765
|
+
context = extract_orchestrate_context(story_content, "STORY-001")
|
|
766
|
+
print(context["status"]) # "SUCCESS" or "FAILURE"
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
story_content: Complete story file content
|
|
770
|
+
story_id: Story ID (e.g., STORY-001)
|
|
771
|
+
workflow_start_time: Optional ISO8601 start timestamp
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Dictionary with keys: workflow_id, story_id, status, total_duration,
|
|
775
|
+
start_time, end_time, phases_executed, quality_gates, checkpoint_info.
|
|
776
|
+
If status is FAILURE, also includes: failed_phase, failure_summary,
|
|
777
|
+
phases_aborted, and optionally qa_attempts.
|
|
778
|
+
"""
|
|
779
|
+
extractor = OrchestrateHooksContextExtractor()
|
|
780
|
+
return extractor.extract_workflow_context(story_content, story_id, workflow_start_time)
|