devforgeai 1.0.5 → 1.0.7

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