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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -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/devforgeai-qa/SKILL.md +1 -1
- package/src/claude/skills/researching-market/SKILL.md +2 -1
- package/src/cli/lib/copier.js +13 -1
- 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,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
|