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.
Files changed (133) hide show
  1. package/CLAUDE.md +120 -0
  2. package/bin/devforgeai.js +0 -0
  3. package/package.json +9 -1
  4. package/src/CLAUDE.md +699 -0
  5. package/src/claude/hooks/phase-completion-gate.sh +0 -0
  6. package/src/claude/scripts/README.md +396 -0
  7. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  8. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  9. package/src/claude/scripts/devforgeai-validate +6 -0
  10. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  11. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  12. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  16. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  17. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  18. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  43. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  44. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  48. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  49. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  50. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  53. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  54. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  55. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  56. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  57. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  58. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  105. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  106. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  110. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  111. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  116. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  117. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  118. package/src/claude/scripts/install_hooks.sh +186 -0
  119. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  120. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  121. package/src/claude/scripts/plan_file_kb.sh +704 -0
  122. package/src/claude/scripts/requirements.txt +8 -0
  123. package/src/claude/scripts/session_catalog.sh +543 -0
  124. package/src/claude/scripts/setup.py +55 -0
  125. package/src/claude/scripts/start-devforgeai.sh +16 -0
  126. package/src/claude/scripts/statusline.sh +27 -0
  127. package/src/claude/scripts/validate_deferrals.py +344 -0
  128. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  132. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  133. 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)