devforgeai 1.0.4 → 1.0.6

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 (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,156 @@
1
+ """
2
+ Pattern matching for AskUserQuestion prompts (STORY-098).
3
+
4
+ AC#2: Answer Matching Logic
5
+ - Regex pattern matching with case insensitivity
6
+ - First match wins
7
+ - Logging of matches when enabled
8
+
9
+ AC#4: Default Answer Fallback
10
+ - first_option: Use first available option
11
+ - skip: Return None (no resolution)
12
+ - fail: Raise HeadlessResolutionError
13
+ """
14
+ import logging
15
+ import re
16
+ from dataclasses import dataclass
17
+ from typing import Dict, List, Optional, Pattern
18
+
19
+ from .exceptions import HeadlessResolutionError
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class MatchResult:
26
+ """Result of pattern matching."""
27
+
28
+ key: str
29
+ answer: str
30
+ pattern: str
31
+ is_default: bool = False
32
+
33
+
34
+ class PromptPatternMatcher:
35
+ """
36
+ Matches AskUserQuestion prompts to configured answers using regex patterns.
37
+
38
+ NFR-001: Answer resolution time < 10ms per prompt lookup
39
+ - Pre-compiles regex patterns on initialization
40
+ - Uses case-insensitive matching
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ patterns: Dict[str, dict],
46
+ default_strategy: str = "fail",
47
+ log_matches: bool = False,
48
+ ):
49
+ """
50
+ Initialize pattern matcher.
51
+
52
+ Args:
53
+ patterns: Dict mapping key to {pattern, answer} entries
54
+ default_strategy: What to do when no match found (fail|first_option|skip)
55
+ log_matches: Whether to log match selections
56
+ """
57
+ self._patterns = patterns
58
+ self._default_strategy = default_strategy
59
+ self._log_matches = log_matches
60
+ self._compiled: Dict[str, Pattern] = self._compile_patterns()
61
+
62
+ def _compile_patterns(self) -> Dict[str, Pattern]:
63
+ """Pre-compile regex patterns for performance."""
64
+ compiled = {}
65
+ for key, entry in self._patterns.items():
66
+ pattern_str = entry.get("pattern", "") if isinstance(entry, dict) else str(entry)
67
+ try:
68
+ compiled[key] = re.compile(pattern_str, re.IGNORECASE)
69
+ except re.error as e:
70
+ logger.warning(f"Invalid regex pattern for '{key}': {pattern_str} - {e}")
71
+ # Use literal match as fallback
72
+ compiled[key] = re.compile(re.escape(pattern_str), re.IGNORECASE)
73
+ return compiled
74
+
75
+ def match(self, prompt_text: str) -> Optional[MatchResult]:
76
+ """
77
+ Match prompt to first matching pattern.
78
+
79
+ AC#2: Answer Matching Logic
80
+ - Returns first matching pattern (order matters)
81
+ - Logs selection if log_matches enabled
82
+
83
+ Args:
84
+ prompt_text: The AskUserQuestion prompt text
85
+
86
+ Returns:
87
+ MatchResult if match found, None otherwise
88
+ """
89
+ for key, pattern in self._compiled.items():
90
+ if pattern.search(prompt_text):
91
+ entry = self._patterns[key]
92
+ answer = entry.get("answer", "") if isinstance(entry, dict) else str(entry)
93
+
94
+ if self._log_matches:
95
+ logger.info(f"CI Mode: Selected '{answer}' for prompt matching pattern '{key}'")
96
+
97
+ return MatchResult(
98
+ key=key,
99
+ answer=answer,
100
+ pattern=entry.get("pattern", "") if isinstance(entry, dict) else str(entry),
101
+ is_default=False,
102
+ )
103
+
104
+ return None
105
+
106
+ def match_with_fallback(
107
+ self, prompt_text: str, options: List[str]
108
+ ) -> Optional[MatchResult]:
109
+ """
110
+ Match prompt with fallback to default strategy.
111
+
112
+ AC#4: Default Answer Fallback
113
+ - first_option: Use first option from provided list
114
+ - skip: Return None
115
+ - fail: Raise HeadlessResolutionError
116
+
117
+ Args:
118
+ prompt_text: The AskUserQuestion prompt text
119
+ options: Available answer options
120
+
121
+ Returns:
122
+ MatchResult if match found or default used, None if skip strategy
123
+
124
+ Raises:
125
+ HeadlessResolutionError: If fail strategy and no match
126
+ """
127
+ # Try exact/regex match first
128
+ result = self.match(prompt_text)
129
+ if result:
130
+ return result
131
+
132
+ # Apply default strategy
133
+ if self._default_strategy == "first_option":
134
+ if options:
135
+ if self._log_matches:
136
+ logger.warning(
137
+ f"Using default answer for unmatched prompt: '{prompt_text[:50]}...'"
138
+ )
139
+ return MatchResult(
140
+ key="_default",
141
+ answer=options[0],
142
+ pattern="",
143
+ is_default=True,
144
+ )
145
+ # No options available, fall through to fail
146
+ self._default_strategy = "fail"
147
+
148
+ if self._default_strategy == "skip":
149
+ if self._log_matches:
150
+ logger.warning(
151
+ f"Skipping unmatched prompt (skip strategy): '{prompt_text[:50]}...'"
152
+ )
153
+ return None
154
+
155
+ # fail strategy (default)
156
+ raise HeadlessResolutionError(prompt_text)
@@ -0,0 +1,313 @@
1
+ """
2
+ DevForgeAI Feedback Hook Service
3
+
4
+ Handles invocation of devforgeai-feedback skill with operation context.
5
+ Implements graceful degradation, timeout protection, and circular invocation guards.
6
+
7
+ Features:
8
+ - Context extraction and sanitization
9
+ - 30-second timeout protection
10
+ - Circular invocation detection via DEVFORGEAI_HOOK_ACTIVE env var
11
+ - Graceful error handling (no exceptions to caller)
12
+ - Comprehensive logging
13
+ """
14
+
15
+ import logging
16
+ import os
17
+ import threading
18
+ import traceback
19
+ from typing import Optional, Dict, Any
20
+
21
+ from .context_extraction import extract_context, sanitize_context
22
+
23
+ # Configure logging
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Constants for timeout protection
27
+ TIMEOUT_SECONDS = 30
28
+ HOOK_ACTIVE_ENV_VAR = "DEVFORGEAI_HOOK_ACTIVE"
29
+
30
+
31
+ class HookInvocationService:
32
+ """Service for invoking devforgeai-feedback skill with operation context."""
33
+
34
+ def __init__(self, timeout: int = TIMEOUT_SECONDS):
35
+ """
36
+ Initialize HookInvocationService.
37
+
38
+ Args:
39
+ timeout: Timeout in seconds (default: 30)
40
+ """
41
+ self.timeout = timeout
42
+ self._timeout_occurred = False
43
+ self._timer = None
44
+
45
+ def invoke(self, operation: str, story_id: Optional[str] = None) -> bool:
46
+ """
47
+ Invoke feedback hook for an operation.
48
+
49
+ Args:
50
+ operation: Operation name (e.g., 'dev', 'qa', 'release')
51
+ story_id: Optional story ID (format: STORY-NNN)
52
+
53
+ Returns:
54
+ True if hook invocation succeeded, False otherwise
55
+ """
56
+ try:
57
+ self._log_invocation_start(operation, story_id)
58
+
59
+ # Check for circular invocation
60
+ if self.check_circular_invocation():
61
+ logger.error("Circular invocation detected, aborting")
62
+ return False
63
+
64
+ # Set hook active flag for nested calls
65
+ self.set_hook_active()
66
+
67
+ try:
68
+ return self._process_hook_invocation(operation, story_id)
69
+
70
+ finally:
71
+ # Clear hook active flag
72
+ self._clear_hook_active()
73
+
74
+ except Exception as e:
75
+ self._log_error(e)
76
+ return False
77
+
78
+ def _process_hook_invocation(
79
+ self, operation: str, story_id: Optional[str]
80
+ ) -> bool:
81
+ """Process the actual hook invocation workflow."""
82
+ # Extract context from operation
83
+ context = extract_context(operation, story_id)
84
+ self._log_context_extraction(context)
85
+
86
+ # Sanitize context (remove secrets)
87
+ context = sanitize_context(context)
88
+
89
+ # Invoke feedback skill with timeout
90
+ return self._invoke_skill_with_timeout(operation, context)
91
+
92
+ @staticmethod
93
+ def _log_invocation_start(operation: str, story_id: Optional[str]) -> None:
94
+ """Log the start of hook invocation."""
95
+ logger.info(f"Invoking feedback hook: operation={operation}, story={story_id}")
96
+
97
+ @staticmethod
98
+ def _log_context_extraction(context: Dict[str, Any]) -> None:
99
+ """Log context extraction completion with metrics."""
100
+ logger.info(
101
+ f"Context extracted: {context.get('context_size_bytes', 0)}B, "
102
+ f"{len(context.get('todos', []))} todos, "
103
+ f"{len(context.get('errors', []))} errors"
104
+ )
105
+
106
+ @staticmethod
107
+ def _log_error(error: Exception) -> None:
108
+ """Log error with stack trace."""
109
+ logger.error(f"Hook invocation failed: {str(error)}")
110
+ logger.debug(traceback.format_exc())
111
+
112
+ def check_circular_invocation(self) -> bool:
113
+ """
114
+ Check if we're in a circular invocation (already inside a hook).
115
+
116
+ Returns:
117
+ True if circular invocation detected, False otherwise
118
+ """
119
+ hook_active = os.environ.get(HOOK_ACTIVE_ENV_VAR)
120
+ return hook_active in ("1", "true", "True")
121
+
122
+ def set_hook_active(self) -> None:
123
+ """Set DEVFORGEAI_HOOK_ACTIVE environment variable to prevent re-entry."""
124
+ os.environ[HOOK_ACTIVE_ENV_VAR] = "1"
125
+
126
+ def _clear_hook_active(self) -> None:
127
+ """Clear DEVFORGEAI_HOOK_ACTIVE environment variable."""
128
+ if HOOK_ACTIVE_ENV_VAR in os.environ:
129
+ del os.environ[HOOK_ACTIVE_ENV_VAR]
130
+
131
+ def invoke_feedback_skill(self, context: Dict[str, Any]) -> bool:
132
+ """
133
+ Invoke devforgeai-feedback skill with pre-populated context.
134
+
135
+ Prints structured output to stdout containing operation context and
136
+ instructions for Claude to invoke the devforgeai-feedback skill.
137
+
138
+ Output Format (v1.0):
139
+ ==============================================================
140
+ FEEDBACK HOOK TRIGGERED
141
+ ==============================================================
142
+ Operation: {operation}
143
+ Operation ID: {operation_id}
144
+ Story ID: {story_id or "N/A"}
145
+ Status: {status}
146
+ Duration: {duration_ms}ms
147
+ Todos: {count} items ({completed} completed, ...)
148
+ Errors: {error_count}
149
+
150
+ Action Required: Invoke devforgeai-feedback skill
151
+ Context: operation={operation}, story={story_id}, status={status}
152
+ ==============================================================
153
+
154
+ Args:
155
+ context: Pre-extracted operation context dictionary containing:
156
+ - operation_id: Unique operation identifier
157
+ - operation: Operation type (dev, qa, release)
158
+ - story_id: Story reference (STORY-NNN) or None
159
+ - status: Operation status (completed, failed, error)
160
+ - duration_ms: Operation duration in milliseconds
161
+ - todos: List of todo items with status
162
+ - errors: List of error dictionaries
163
+
164
+ Returns:
165
+ True if output generation succeeded, False on any exception
166
+ """
167
+ try:
168
+ # Validate context is a dictionary
169
+ if context is None or not isinstance(context, dict):
170
+ error_msg = ("Invalid context: expected dict, got " +
171
+ f"{type(context).__name__ if context is not None else 'None'}")
172
+ logger.error(error_msg)
173
+ # Log error details at debug level for debugging
174
+ logger.debug(f"Context validation error - Traceback: {error_msg}")
175
+ return False
176
+
177
+ # Extract context fields with safe defaults
178
+ operation_id = self._escape_value(context.get("operation_id", "unknown"))
179
+ operation = self._escape_value(context.get("operation", "unknown"))
180
+ story_id = context.get("story_id")
181
+ story_id_display = self._escape_value(story_id) if story_id else "N/A"
182
+ story_id_context = self._escape_value(story_id) if story_id else "unassigned"
183
+ status = self._escape_value(context.get("status", "unknown"))
184
+ duration_ms = context.get("duration_ms", 0)
185
+
186
+ # Calculate todos summary
187
+ todos_list = context.get("todos", [])
188
+ if not isinstance(todos_list, list):
189
+ todos_list = []
190
+ todos_count = len(todos_list)
191
+ completed = sum(1 for t in todos_list
192
+ if isinstance(t, dict) and t.get("status") == "completed")
193
+ in_progress = sum(1 for t in todos_list
194
+ if isinstance(t, dict) and t.get("status") == "in_progress")
195
+ pending = sum(1 for t in todos_list
196
+ if isinstance(t, dict) and t.get("status") == "pending")
197
+
198
+ # Calculate errors count
199
+ errors_list = context.get("errors", [])
200
+ if not isinstance(errors_list, list):
201
+ errors_list = []
202
+ error_count = len(errors_list)
203
+
204
+ # Build and print structured output
205
+ delimiter = "=" * 62
206
+ output_lines = [
207
+ delimiter,
208
+ " FEEDBACK HOOK TRIGGERED",
209
+ delimiter,
210
+ f" Operation: {operation}",
211
+ f" Operation ID: {operation_id}",
212
+ f" Story ID: {story_id_display}",
213
+ f" Status: {status}",
214
+ f" Duration: {duration_ms}ms",
215
+ f" Todos: {todos_count} items ({completed} completed, "
216
+ f"{in_progress} in progress, {pending} pending)",
217
+ f" Errors: {error_count}",
218
+ "",
219
+ " Action Required: Invoke devforgeai-feedback skill",
220
+ f" Context: operation={operation}, story={story_id_context}, status={status}",
221
+ delimiter,
222
+ ]
223
+
224
+ # Print to stdout
225
+ print("\n".join(output_lines))
226
+
227
+ # Log success
228
+ logger.info(f"Feedback hook output generated for operation: {operation_id}")
229
+
230
+ return True
231
+
232
+ except Exception as e:
233
+ logger.error(f"Failed to generate feedback hook output: {str(e)}")
234
+ logger.debug(traceback.format_exc())
235
+ return False
236
+
237
+ @staticmethod
238
+ def _escape_value(value: Any) -> str:
239
+ """
240
+ Escape special characters in context values for safe output.
241
+
242
+ Handles newlines, tabs, quotes, and other special characters
243
+ to ensure parsable output format.
244
+
245
+ Args:
246
+ value: The value to escape (converted to string if needed)
247
+
248
+ Returns:
249
+ Escaped string safe for structured output
250
+ """
251
+ if value is None:
252
+ return ""
253
+ str_value = str(value)
254
+ # Escape special characters
255
+ str_value = str_value.replace("\\", "\\\\") # Backslash first
256
+ str_value = str_value.replace("\n", "\\n") # Newlines
257
+ str_value = str_value.replace("\r", "\\r") # Carriage returns
258
+ str_value = str_value.replace("\t", "\\t") # Tabs
259
+ str_value = str_value.replace('"', '\\"') # Double quotes
260
+ return str_value
261
+
262
+ def _invoke_skill_with_timeout(self, operation: str, context: Dict[str, Any]) -> bool:
263
+ """
264
+ Invoke skill with timeout protection.
265
+
266
+ Args:
267
+ operation: Operation name
268
+ context: Pre-extracted operation context
269
+
270
+ Returns:
271
+ True if skill invocation succeeded, False on timeout or error
272
+ """
273
+ self._timeout_occurred = False
274
+
275
+ # Create and start skill invocation thread
276
+ thread = self._create_skill_thread(context)
277
+ thread.start()
278
+
279
+ # Wait for completion with timeout
280
+ thread.join(timeout=self.timeout)
281
+
282
+ # Check if thread is still alive (timeout occurred)
283
+ if thread.is_alive():
284
+ self._timeout_occurred = True
285
+ logger.error(f"Feedback hook timeout after {self.timeout}s")
286
+ return False
287
+
288
+ return True
289
+
290
+ def _create_skill_thread(self, context: Dict[str, Any]) -> threading.Thread:
291
+ """Create a daemon thread for skill invocation."""
292
+ def skill_thread():
293
+ try:
294
+ self.invoke_feedback_skill(context)
295
+ except Exception:
296
+ pass
297
+
298
+ return threading.Thread(target=skill_thread, daemon=True)
299
+
300
+
301
+ def invoke_hooks(operation: str, story_id: Optional[str] = None) -> bool:
302
+ """
303
+ Public API for invoking feedback hooks.
304
+
305
+ Args:
306
+ operation: Operation name (e.g., 'dev', 'qa', 'release')
307
+ story_id: Optional story ID (format: STORY-NNN)
308
+
309
+ Returns:
310
+ True if hook invocation succeeded, False otherwise
311
+ """
312
+ service = HookInvocationService(timeout=TIMEOUT_SECONDS)
313
+ return service.invoke(operation, story_id)
@@ -0,0 +1,46 @@
1
+ """
2
+ DevForgeAI Workflow Success Metrics package.
3
+
4
+ Provides functions for calculating per-command metrics,
5
+ failure mode analysis, and story point segmentation.
6
+
7
+ STORY-227: Calculate Workflow Success Metrics
8
+ """
9
+ from devforgeai_cli.metrics.command_metrics import (
10
+ calculate_completion_rate,
11
+ calculate_error_rate,
12
+ calculate_retry_rate,
13
+ calculate_per_command_metrics,
14
+ )
15
+ from devforgeai_cli.metrics.failure_modes import (
16
+ identify_failure_modes,
17
+ rank_failure_modes,
18
+ categorize_failure_mode,
19
+ get_failure_mode_summary,
20
+ )
21
+ from devforgeai_cli.metrics.story_segmentation import (
22
+ get_valid_story_points,
23
+ is_valid_story_point,
24
+ segment_metrics_by_story_points,
25
+ calculate_segment_averages,
26
+ get_segmentation_summary,
27
+ )
28
+
29
+ __all__ = [
30
+ # AC#1: Per-command metrics
31
+ "calculate_completion_rate",
32
+ "calculate_error_rate",
33
+ "calculate_retry_rate",
34
+ "calculate_per_command_metrics",
35
+ # AC#2: Failure mode identification
36
+ "identify_failure_modes",
37
+ "rank_failure_modes",
38
+ "categorize_failure_mode",
39
+ "get_failure_mode_summary",
40
+ # AC#3: Story segmentation
41
+ "get_valid_story_points",
42
+ "is_valid_story_point",
43
+ "segment_metrics_by_story_points",
44
+ "calculate_segment_averages",
45
+ "get_segmentation_summary",
46
+ ]
@@ -0,0 +1,142 @@
1
+ """
2
+ Per-command metrics calculation module.
3
+
4
+ AC#1: Calculate completion rate, error rate, and retry rate per command type.
5
+
6
+ STORY-227: Calculate Workflow Success Metrics
7
+ """
8
+ from typing import Any, Dict, List
9
+
10
+
11
+ def calculate_completion_rate(data: List[Dict[str, Any]], command_type: str) -> float:
12
+ """
13
+ Calculate completion rate for a specific command type.
14
+
15
+ Args:
16
+ data: List of command execution entries with 'command' and 'status' fields.
17
+ command_type: The command type to filter (e.g., "/dev", "/qa").
18
+
19
+ Returns:
20
+ Percentage of completed executions (0-100).
21
+ Returns 0.0 for empty data or nonexistent command.
22
+ """
23
+ if not data:
24
+ return 0.0
25
+
26
+ # Filter for the specific command type
27
+ command_entries = [entry for entry in data if entry.get("command") == command_type]
28
+
29
+ if not command_entries:
30
+ return 0.0
31
+
32
+ # Count completed entries
33
+ completed_count = sum(
34
+ 1 for entry in command_entries if entry.get("status") == "completed"
35
+ )
36
+
37
+ # Calculate percentage
38
+ return (completed_count / len(command_entries)) * 100.0
39
+
40
+
41
+ def calculate_error_rate(data: List[Dict[str, Any]], command_type: str) -> float:
42
+ """
43
+ Calculate error rate for a specific command type.
44
+
45
+ Args:
46
+ data: List of command execution entries with 'command' and 'status' fields.
47
+ command_type: The command type to filter (e.g., "/dev", "/qa").
48
+
49
+ Returns:
50
+ Percentage of failed executions (0-100).
51
+ Returns 0.0 for empty data or nonexistent command.
52
+ """
53
+ if not data:
54
+ return 0.0
55
+
56
+ # Filter for the specific command type
57
+ command_entries = [entry for entry in data if entry.get("command") == command_type]
58
+
59
+ if not command_entries:
60
+ return 0.0
61
+
62
+ # Count error entries
63
+ error_count = sum(
64
+ 1 for entry in command_entries if entry.get("status") == "error"
65
+ )
66
+
67
+ # Calculate percentage
68
+ return round((error_count / len(command_entries)) * 100.0, 2)
69
+
70
+
71
+ def calculate_retry_rate(data: List[Dict[str, Any]], command_type: str) -> float:
72
+ """
73
+ Calculate retry rate for a specific command type.
74
+
75
+ Args:
76
+ data: List of command execution entries with 'command' and 'retry_count' fields.
77
+ command_type: The command type to filter (e.g., "/dev", "/qa").
78
+
79
+ Returns:
80
+ Percentage of executions that had retries (retry_count > 0).
81
+ Returns 0.0 for empty data or nonexistent command.
82
+ """
83
+ if not data:
84
+ return 0.0
85
+
86
+ # Filter for the specific command type
87
+ command_entries = [entry for entry in data if entry.get("command") == command_type]
88
+
89
+ if not command_entries:
90
+ return 0.0
91
+
92
+ # Count entries with retries
93
+ retry_count = sum(
94
+ 1 for entry in command_entries if entry.get("retry_count", 0) > 0
95
+ )
96
+
97
+ # Calculate percentage
98
+ return round((retry_count / len(command_entries)) * 100.0, 2)
99
+
100
+
101
+ def calculate_per_command_metrics(data: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
102
+ """
103
+ Calculate metrics for all command types in data.
104
+
105
+ Args:
106
+ data: List of command execution entries.
107
+
108
+ Returns:
109
+ Dictionary with metrics per command type:
110
+ {
111
+ "/dev": {
112
+ "completion_rate": float,
113
+ "error_rate": float,
114
+ "retry_rate": float,
115
+ "total_executions": int
116
+ },
117
+ ...
118
+ }
119
+ Returns empty dict for empty data.
120
+ """
121
+ if not data:
122
+ return {}
123
+
124
+ # Extract unique command types
125
+ command_types = set(entry.get("command") for entry in data if entry.get("command"))
126
+
127
+ result: Dict[str, Dict[str, Any]] = {}
128
+
129
+ for command_type in command_types:
130
+ # Count total executions for this command
131
+ command_entries = [
132
+ entry for entry in data if entry.get("command") == command_type
133
+ ]
134
+
135
+ result[command_type] = {
136
+ "completion_rate": calculate_completion_rate(data, command_type),
137
+ "error_rate": calculate_error_rate(data, command_type),
138
+ "retry_rate": calculate_retry_rate(data, command_type),
139
+ "total_executions": len(command_entries),
140
+ }
141
+
142
+ return result