devforgeai 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +120 -0
- package/bin/devforgeai.js +0 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/hooks/phase-completion-gate.sh +0 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DevForgeAI CLI Commands Module"""
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DevForgeAI check-hooks CLI Command
|
|
4
|
+
|
|
5
|
+
Validates whether hooks should trigger based on:
|
|
6
|
+
- Global enabled/disabled status
|
|
7
|
+
- Trigger rules (all/failures-only/none)
|
|
8
|
+
- Operation-specific overrides
|
|
9
|
+
- Circular invocation detection
|
|
10
|
+
|
|
11
|
+
Exit Codes:
|
|
12
|
+
0 - Hooks should trigger
|
|
13
|
+
1 - Hooks should not trigger (or disabled/missing config)
|
|
14
|
+
2 - Error (invalid arguments or config error)
|
|
15
|
+
|
|
16
|
+
Story: STORY-021 - Implement devforgeai check-hooks CLI command
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import logging
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, Optional, Any
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
# Exit codes (exported for use in tests)
|
|
27
|
+
EXIT_CODE_TRIGGER = 0
|
|
28
|
+
EXIT_CODE_DONT_TRIGGER = 1
|
|
29
|
+
EXIT_CODE_ERROR = 2
|
|
30
|
+
|
|
31
|
+
# Configure logger - uses logger hierarchy for DevForgeAI CLI context
|
|
32
|
+
logger = logging.getLogger("devforgeai_cli.commands.check_hooks")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CheckHooksValidator:
|
|
36
|
+
"""Validator for hook configuration and trigger rules."""
|
|
37
|
+
|
|
38
|
+
# Valid trigger_on values
|
|
39
|
+
VALID_TRIGGER_ON = {"all", "failures-only", "none"}
|
|
40
|
+
# Valid status values
|
|
41
|
+
VALID_STATUSES = {"success", "failure", "partial"}
|
|
42
|
+
# Valid hook_type values (STORY-185)
|
|
43
|
+
VALID_HOOK_TYPES = {"user", "ai", "all"}
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: Dict[str, Any], hook_type: str = "all"):
|
|
46
|
+
"""
|
|
47
|
+
Initialize validator with configuration.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Hook configuration dictionary
|
|
51
|
+
hook_type: Filter hooks by type ('user', 'ai', or 'all') (STORY-185)
|
|
52
|
+
"""
|
|
53
|
+
self.config = config or {}
|
|
54
|
+
self.hook_type = hook_type # STORY-185: Store hook_type for filtering
|
|
55
|
+
self.enabled = self.config.get("enabled", False)
|
|
56
|
+
self.global_rules = self.config.get("global_rules") or {}
|
|
57
|
+
self.operations = self.config.get("operations") or {}
|
|
58
|
+
self.hooks = self._filter_hooks_by_type() # STORY-185: Filter hooks
|
|
59
|
+
|
|
60
|
+
def _filter_hooks_by_type(self) -> list:
|
|
61
|
+
"""
|
|
62
|
+
Filter hooks list by hook_type (STORY-185: AC-3).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Filtered list of hooks matching hook_type, or all hooks if type is 'all'
|
|
66
|
+
"""
|
|
67
|
+
hooks = self.config.get("hooks", [])
|
|
68
|
+
if self.hook_type == "all":
|
|
69
|
+
return hooks
|
|
70
|
+
return [h for h in hooks if h.get("hook_type") == self.hook_type]
|
|
71
|
+
|
|
72
|
+
def _is_valid_enum(self, value: str, allowed_set: set, field_name: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Validate that value is in allowed set of enum values.
|
|
75
|
+
|
|
76
|
+
Helper method to reduce duplication in enum validation.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
value: Value to validate
|
|
80
|
+
allowed_set: Set of allowed enum values
|
|
81
|
+
field_name: Name of field being validated (for error messages)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if valid, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
return value in allowed_set
|
|
87
|
+
|
|
88
|
+
def validate_status(self, status: str) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Validate that status is one of the allowed values.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
status: Status value to validate
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if valid, False otherwise
|
|
97
|
+
"""
|
|
98
|
+
return self._is_valid_enum(status, self.VALID_STATUSES, "status")
|
|
99
|
+
|
|
100
|
+
def validate_trigger_on(self, trigger_on: str) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Validate that trigger_on is one of the allowed values.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
trigger_on: Trigger rule to validate
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if valid, False otherwise
|
|
109
|
+
"""
|
|
110
|
+
return self._is_valid_enum(trigger_on, self.VALID_TRIGGER_ON, "trigger_on")
|
|
111
|
+
|
|
112
|
+
def validate(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Validate the entire configuration schema.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If configuration is invalid
|
|
118
|
+
"""
|
|
119
|
+
# Check global_rules trigger_on value
|
|
120
|
+
if self.global_rules:
|
|
121
|
+
trigger_on = self.global_rules.get("trigger_on")
|
|
122
|
+
if trigger_on and not self.validate_trigger_on(trigger_on):
|
|
123
|
+
raise ValueError(f"Invalid trigger_on value: {trigger_on}")
|
|
124
|
+
|
|
125
|
+
# Check operation-specific trigger_on values
|
|
126
|
+
for op_name, op_config in self.operations.items():
|
|
127
|
+
if isinstance(op_config, dict):
|
|
128
|
+
trigger_on = op_config.get("trigger_on")
|
|
129
|
+
if trigger_on and not self.validate_trigger_on(trigger_on):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Invalid trigger_on value for operation '{op_name}': {trigger_on}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def get_trigger_rule(self, operation: str) -> Optional[str]:
|
|
135
|
+
"""
|
|
136
|
+
Get the trigger rule for an operation (with fallback to global).
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
operation: Operation name
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Trigger rule string (all/failures-only/none) or None
|
|
143
|
+
"""
|
|
144
|
+
# Check for operation-specific override
|
|
145
|
+
if operation in self.operations:
|
|
146
|
+
op_config = self.operations[operation]
|
|
147
|
+
if isinstance(op_config, dict) and "trigger_on" in op_config:
|
|
148
|
+
return op_config["trigger_on"]
|
|
149
|
+
|
|
150
|
+
# Fall back to global rule
|
|
151
|
+
if self.global_rules and "trigger_on" in self.global_rules:
|
|
152
|
+
return self.global_rules["trigger_on"]
|
|
153
|
+
|
|
154
|
+
# Default: don't trigger
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def should_trigger(self, operation: str, status: str) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Determine if hook should trigger based on rules.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
operation: Operation name
|
|
163
|
+
status: Operation status (success/failure/partial)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if hook should trigger, False otherwise
|
|
167
|
+
"""
|
|
168
|
+
trigger_rule = self.get_trigger_rule(operation)
|
|
169
|
+
|
|
170
|
+
if trigger_rule is None:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
if trigger_rule == "all":
|
|
174
|
+
# Trigger on any status
|
|
175
|
+
return True
|
|
176
|
+
elif trigger_rule == "failures-only":
|
|
177
|
+
# Trigger only on failure or partial (not success)
|
|
178
|
+
return status in {"failure", "partial"}
|
|
179
|
+
elif trigger_rule == "none":
|
|
180
|
+
# Never trigger
|
|
181
|
+
return False
|
|
182
|
+
else:
|
|
183
|
+
# Invalid trigger rule - don't trigger as safe default
|
|
184
|
+
logger.warning(f"Invalid trigger_on rule: {trigger_rule}")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_config(config_path: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
189
|
+
"""
|
|
190
|
+
Load hook configuration from YAML file.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
config_path: Path to hooks.yaml config file.
|
|
194
|
+
If None, uses default: devforgeai/config/hooks.yaml
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Configuration dictionary or None if file not found/invalid
|
|
198
|
+
"""
|
|
199
|
+
if config_path is None:
|
|
200
|
+
config_path = "devforgeai/config/hooks.yaml"
|
|
201
|
+
|
|
202
|
+
config_path = str(config_path)
|
|
203
|
+
|
|
204
|
+
# Check if file exists
|
|
205
|
+
if not os.path.exists(config_path):
|
|
206
|
+
logger.warning(f"Hooks config not found at {config_path}, assuming disabled")
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with open(config_path, "r") as f:
|
|
211
|
+
config = yaml.safe_load(f)
|
|
212
|
+
|
|
213
|
+
if config is None:
|
|
214
|
+
logger.warning(f"Empty hooks config file: {config_path}")
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
return config
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
# Consolidated exception handling for YAML parsing, file I/O, and unexpected errors
|
|
221
|
+
logger.error(f"Failed to load hooks config from {config_path}: {e}")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _validate_required_string_arg(arg_value: Any, arg_name: str) -> Optional[str]:
|
|
226
|
+
"""
|
|
227
|
+
Validate that argument is a non-empty string.
|
|
228
|
+
|
|
229
|
+
Helper to reduce duplication in argument validation.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
arg_value: Argument value to validate
|
|
233
|
+
arg_name: Name of argument for error messages
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Stripped string if valid, None if invalid
|
|
237
|
+
"""
|
|
238
|
+
if not arg_value or not isinstance(arg_value, str) or not arg_value.strip():
|
|
239
|
+
logger.error(f"Invalid {arg_name}: {arg_name} is required and must be non-empty")
|
|
240
|
+
return None
|
|
241
|
+
return arg_value.strip()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def check_hooks_command(
|
|
245
|
+
operation: str,
|
|
246
|
+
status: str,
|
|
247
|
+
config_path: Optional[str] = None,
|
|
248
|
+
hook_type: str = "all",
|
|
249
|
+
) -> int:
|
|
250
|
+
"""
|
|
251
|
+
Main check-hooks command implementation.
|
|
252
|
+
|
|
253
|
+
Determines if hooks should trigger based on configuration and rules.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
operation: Operation name (e.g., 'dev', 'qa', 'release')
|
|
257
|
+
status: Operation status ('success', 'failure', or 'partial')
|
|
258
|
+
config_path: Path to hooks.yaml config file (optional)
|
|
259
|
+
hook_type: Hook type filter ('user', 'ai', or 'all') (STORY-185)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Exit code:
|
|
263
|
+
0 - Hooks should trigger
|
|
264
|
+
1 - Hooks should not trigger
|
|
265
|
+
2 - Error (invalid arguments)
|
|
266
|
+
"""
|
|
267
|
+
# AC7: Check for circular invocation
|
|
268
|
+
if os.environ.get("DEVFORGEAI_HOOK_ACTIVE"):
|
|
269
|
+
logger.warning("Circular invocation detected (DEVFORGEAI_HOOK_ACTIVE set), skipping hook")
|
|
270
|
+
return EXIT_CODE_DONT_TRIGGER
|
|
271
|
+
|
|
272
|
+
# AC6: Validate arguments - operation must be non-empty string
|
|
273
|
+
operation = _validate_required_string_arg(operation, "operation")
|
|
274
|
+
if operation is None:
|
|
275
|
+
return EXIT_CODE_ERROR
|
|
276
|
+
|
|
277
|
+
# AC6: Validate arguments - status must be non-empty string
|
|
278
|
+
status = _validate_required_string_arg(status, "status")
|
|
279
|
+
if status is None:
|
|
280
|
+
return EXIT_CODE_ERROR
|
|
281
|
+
|
|
282
|
+
# Validate status against allowed values
|
|
283
|
+
if status not in CheckHooksValidator.VALID_STATUSES:
|
|
284
|
+
logger.error(
|
|
285
|
+
f"Invalid status: '{status}' must be one of {CheckHooksValidator.VALID_STATUSES}"
|
|
286
|
+
)
|
|
287
|
+
return EXIT_CODE_ERROR
|
|
288
|
+
|
|
289
|
+
# STORY-185: Validate hook_type against allowed values
|
|
290
|
+
if hook_type not in CheckHooksValidator.VALID_HOOK_TYPES:
|
|
291
|
+
logger.error(
|
|
292
|
+
f"Invalid hook_type: '{hook_type}' must be one of {CheckHooksValidator.VALID_HOOK_TYPES}"
|
|
293
|
+
)
|
|
294
|
+
return EXIT_CODE_ERROR
|
|
295
|
+
|
|
296
|
+
# Load configuration
|
|
297
|
+
config = load_config(config_path)
|
|
298
|
+
|
|
299
|
+
# AC5: Handle missing config
|
|
300
|
+
if config is None:
|
|
301
|
+
# Config not found or empty - treat as disabled
|
|
302
|
+
return EXIT_CODE_DONT_TRIGGER
|
|
303
|
+
|
|
304
|
+
# AC1: Check if hooks are enabled
|
|
305
|
+
if not config.get("enabled", False):
|
|
306
|
+
logger.warning("Hooks are disabled in configuration")
|
|
307
|
+
return EXIT_CODE_DONT_TRIGGER
|
|
308
|
+
|
|
309
|
+
# Create validator with hook_type filtering (STORY-185: AC-3)
|
|
310
|
+
try:
|
|
311
|
+
validator = CheckHooksValidator(config, hook_type=hook_type)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to initialize hooks validator: {e}")
|
|
314
|
+
return EXIT_CODE_ERROR
|
|
315
|
+
|
|
316
|
+
# AC2 & AC3: Evaluate trigger rules and operation-specific overrides
|
|
317
|
+
if validator.should_trigger(operation, status):
|
|
318
|
+
return EXIT_CODE_TRIGGER
|
|
319
|
+
else:
|
|
320
|
+
return EXIT_CODE_DONT_TRIGGER
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _create_argument_parser() -> "argparse.ArgumentParser":
|
|
324
|
+
"""
|
|
325
|
+
Create and configure argument parser for check-hooks command.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Configured ArgumentParser instance
|
|
329
|
+
"""
|
|
330
|
+
import argparse
|
|
331
|
+
|
|
332
|
+
parser = argparse.ArgumentParser(
|
|
333
|
+
description="Check if hooks should trigger for an operation",
|
|
334
|
+
prog="devforgeai check-hooks",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
parser.add_argument(
|
|
338
|
+
"--operation",
|
|
339
|
+
required=True,
|
|
340
|
+
help="Operation name (e.g., dev, qa, release)",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
parser.add_argument(
|
|
344
|
+
"--status",
|
|
345
|
+
required=True,
|
|
346
|
+
choices=["success", "failure", "partial"],
|
|
347
|
+
help="Operation status",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
parser.add_argument(
|
|
351
|
+
"--config",
|
|
352
|
+
default=None,
|
|
353
|
+
help="Path to hooks.yaml config file (default: devforgeai/config/hooks.yaml)",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# STORY-185: Add --type argument for hook type filtering
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"--type",
|
|
359
|
+
type=str,
|
|
360
|
+
choices=["user", "ai", "all"],
|
|
361
|
+
default="all",
|
|
362
|
+
help="Hook type to check (user, ai, or all)",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return parser
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
"""CLI entry point for check-hooks command."""
|
|
370
|
+
parser = _create_argument_parser()
|
|
371
|
+
args = parser.parse_args()
|
|
372
|
+
|
|
373
|
+
exit_code = check_hooks_command(
|
|
374
|
+
operation=args.operation,
|
|
375
|
+
status=args.status,
|
|
376
|
+
config_path=args.config,
|
|
377
|
+
hook_type=args.type, # STORY-185: Pass hook_type from CLI
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
sys.exit(exit_code)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
main()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevForgeAI invoke-hooks CLI Command
|
|
3
|
+
|
|
4
|
+
Handles the 'devforgeai invoke-hooks' command for triggering feedback hooks.
|
|
5
|
+
|
|
6
|
+
Command: devforgeai invoke-hooks --operation <op> [--story <story>] [--verbose]
|
|
7
|
+
|
|
8
|
+
Arguments:
|
|
9
|
+
--operation: Operation name (required, e.g., dev, qa, release)
|
|
10
|
+
--story: Story ID (optional, format: STORY-NNN)
|
|
11
|
+
--verbose: Verbose logging output (optional, flag)
|
|
12
|
+
|
|
13
|
+
Exit Codes:
|
|
14
|
+
0: Success
|
|
15
|
+
1: Failure
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
# Exit codes
|
|
23
|
+
EXIT_CODE_SUCCESS = 0
|
|
24
|
+
EXIT_CODE_FAILURE = 1
|
|
25
|
+
|
|
26
|
+
# Validation constants
|
|
27
|
+
STORY_ID_PATTERN = r"^STORY-\d{3,}$"
|
|
28
|
+
|
|
29
|
+
# Configure logging
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def invoke_hooks_command(
|
|
34
|
+
operation: str, story_id: Optional[str] = None, verbose: bool = False
|
|
35
|
+
) -> int:
|
|
36
|
+
"""
|
|
37
|
+
CLI command handler for 'devforgeai invoke-hooks'.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
operation: Operation name (required)
|
|
41
|
+
story_id: Optional story ID (format: STORY-NNN)
|
|
42
|
+
verbose: Enable verbose logging
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Exit code (0 for success, 1 for failure)
|
|
46
|
+
"""
|
|
47
|
+
_configure_logging(verbose)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Validate arguments
|
|
51
|
+
if not _validate_operation(operation):
|
|
52
|
+
return EXIT_CODE_FAILURE
|
|
53
|
+
|
|
54
|
+
story_id = _validate_and_normalize_story_id(story_id)
|
|
55
|
+
|
|
56
|
+
# Import here to avoid circular imports
|
|
57
|
+
from ..hooks import invoke_hooks
|
|
58
|
+
|
|
59
|
+
# Invoke the hook and return result
|
|
60
|
+
return _execute_hook_invocation(operation, story_id, invoke_hooks)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Unexpected error in invoke-hooks command: {str(e)}")
|
|
64
|
+
logger.debug("", exc_info=True)
|
|
65
|
+
return EXIT_CODE_FAILURE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _configure_logging(verbose: bool) -> None:
|
|
69
|
+
"""Configure logging level based on verbosity flag."""
|
|
70
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
71
|
+
logging.basicConfig(level=level)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _validate_operation(operation: str) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Validate operation argument.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
operation: Operation name to validate
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if valid, False otherwise
|
|
83
|
+
"""
|
|
84
|
+
if not operation:
|
|
85
|
+
logger.error("--operation argument is required")
|
|
86
|
+
return False
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_and_normalize_story_id(story_id: Optional[str]) -> Optional[str]:
|
|
91
|
+
"""
|
|
92
|
+
Validate and normalize story ID.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
story_id: Story ID to validate
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Normalized story ID or None if invalid
|
|
99
|
+
"""
|
|
100
|
+
if not story_id:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if not _validate_story_id_format(story_id):
|
|
104
|
+
logger.warning(f"Invalid story ID format: {story_id}, continuing with story_id=None")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return story_id
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _execute_hook_invocation(
|
|
111
|
+
operation: str,
|
|
112
|
+
story_id: Optional[str],
|
|
113
|
+
invoke_hooks_fn,
|
|
114
|
+
) -> int:
|
|
115
|
+
"""
|
|
116
|
+
Execute hook invocation and return appropriate exit code.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
operation: Operation name
|
|
120
|
+
story_id: Optional story ID
|
|
121
|
+
invoke_hooks_fn: Function to invoke hooks
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Exit code (0 for success, 1 for failure)
|
|
125
|
+
"""
|
|
126
|
+
success = invoke_hooks_fn(operation, story_id)
|
|
127
|
+
|
|
128
|
+
if success:
|
|
129
|
+
logger.info(f"Feedback hook completed successfully: {operation}")
|
|
130
|
+
return EXIT_CODE_SUCCESS
|
|
131
|
+
else:
|
|
132
|
+
logger.error(f"Feedback hook failed: {operation}")
|
|
133
|
+
return EXIT_CODE_FAILURE
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _validate_story_id_format(story_id: str) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Validate story ID format (STORY-NNN where N is digit).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
story_id: Story ID to validate
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if valid format, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
if not story_id:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
return bool(re.match(STORY_ID_PATTERN, story_id))
|